事务

Redis的单条命令是保证原子性的,但是redis事务不能保证原子性

Redis事务本质:一组命令的集合。 ————————- 队列 set set set 执行 —————————- 事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。

  • 一次性
  • 顺序性
  • 排他性
  1. Redis事务没有隔离级别的概念
  2. Redis单条命令是保证原子性的,但是事务不保证原子性!

Redis事务操作过程

  • 开启事务(multi
  • 命令入队
  • 执行事务(exec

所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。

  1. 127.0.0.1:6379> multi # 开启事务
  2. OK
  3. 127.0.0.1:6379> set k1 v1 # 命令入队
  4. QUEUED
  5. 127.0.0.1:6379> set k2 v2 # ..
  6. QUEUED
  7. 127.0.0.1:6379> get k1
  8. QUEUED
  9. 127.0.0.1:6379> set k3 v3
  10. QUEUED
  11. 127.0.0.1:6379> keys *
  12. QUEUED
  13. 127.0.0.1:6379> exec # 事务执行
  14. 1) OK
  15. 2) OK
  16. 3) "v1"
  17. 4) OK
  18. 5) 1) "k3"
  19. 2) "k2"
  20. 3) "k1"
  21. 1234567891011121314151617181920

取消事务(**discurd**)

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379> set k1 v1
  4. QUEUED
  5. 127.0.0.1:6379> set k2 v2
  6. QUEUED
  7. 127.0.0.1:6379> DISCARD # 放弃事务
  8. OK
  9. 127.0.0.1:6379> EXEC
  10. (error) ERR EXEC without MULTI # 当前未开启事务
  11. 127.0.0.1:6379> get k1 # 被放弃事务中命令并未执行
  12. (nil)
  13. 123456789101112

事务错误

代码语法错误(编译时异常)所有的命令都不执行

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379> set k1 v1
  4. QUEUED
  5. 127.0.0.1:6379> set k2 v2
  6. QUEUED
  7. 127.0.0.1:6379> error k1 # 这是一条语法错误命令
  8. (error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队
  9. 127.0.0.1:6379> get k2
  10. QUEUED
  11. 127.0.0.1:6379> EXEC
  12. (error) EXECABORT Transaction discarded because of previous errors. # 执行报错
  13. 127.0.0.1:6379> get k1
  14. (nil) # 其他命令并没有被执行
  15. 1234567891011121314

代码逻辑错误 (运行时异常) 其他命令可以正常执行 >>> 所以不保证事务原子性

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379> set k1 v1
  4. QUEUED
  5. 127.0.0.1:6379> set k2 v2
  6. QUEUED
  7. 127.0.0.1:6379> INCR k1 # 这条命令逻辑错误(对字符串进行增量)
  8. QUEUED
  9. 127.0.0.1:6379> get k2
  10. QUEUED
  11. 127.0.0.1:6379> exec
  12. 1) OK
  13. 2) OK
  14. 3) (error) ERR value is not an integer or out of range # 运行时报错
  15. 4) "v2" # 其他命令正常执行
  16. # 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
  17. # 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。
  18. 123456789101112131415161718

监控

悲观锁:

  • 很悲观,认为什么时候都会出现问题,无论做什么都会加锁

乐观锁:

  • 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • 获取version
  • 更新的时候比较version

使用watch key监控指定数据,相当于乐观锁加锁。

正常执行

  1. 127.0.0.1:6379> set money 100 # 设置余额:100
  2. OK
  3. 127.0.0.1:6379> set use 0 # 支出使用:0
  4. OK
  5. 127.0.0.1:6379> watch money # 监视money (上锁)
  6. OK
  7. 127.0.0.1:6379> multi
  8. OK
  9. 127.0.0.1:6379> DECRBY money 20
  10. QUEUED
  11. 127.0.0.1:6379> INCRBY use 20
  12. QUEUED
  13. 127.0.0.1:6379> exec # 监视值没有被中途修改,事务正常执行
  14. 1) (integer) 80
  15. 2) (integer) 20
  16. 123456789101112131415

测试多线程修改值,使用watch可以当做redis的乐观锁操作(相当于getversion)

我们启动另外一个客户端模拟插队线程。

线程1:

  1. 127.0.0.1:6379> watch money # money上锁
  2. OK
  3. 127.0.0.1:6379> multi
  4. OK
  5. 127.0.0.1:6379> DECRBY money 20
  6. QUEUED
  7. 127.0.0.1:6379> INCRBY use 20
  8. QUEUED
  9. 127.0.0.1:6379> # 此时事务并没有执行
  10. 123456789

模拟线程插队,线程2:

  1. 127.0.0.1:6379> INCRBY money 500 # 修改了线程一中监视的money
  2. (integer) 600
  3. 12

回到线程1,执行事务:

  1. 127.0.0.1:6379> EXEC # 执行之前,另一个线程修改了我们的值,这个时候就会导致事务执行失败
  2. (nil) # 没有结果,说明事务执行失败
  3. 127.0.0.1:6379> get money # 线程2 修改生效
  4. "600"
  5. 127.0.0.1:6379> get use # 线程1事务执行失败,数值没有被修改
  6. "0"
  7. 1234567

解锁获取最新值,然后再加锁进行事务。 unwatch进行解锁。

注意:每次提交执行exec后都会自动释放锁,不管是否成功

SpringBoot整合

  1. 导入依赖
  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>
  1. 编写配置文件

    1. # 配置redis
    2. spring.redis.host=39.99.xxx.xx
    3. spring.redis.port=637
  2. 使用RedisTemplate

    1. @SpringBootTest
    2. class Redis02SpringbootApplicationTests {
    3. @Autowired
    4. private RedisTemplate redisTemplate;
    5. @Test
    6. void contextLoads() {
    7. // redisTemplate 操作不同的数据类型,api和我们的指令是一样的
    8. // opsForValue 操作字符串 类似String
    9. // opsForList 操作List 类似List
    10. // opsForHah
    11. // 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
    12. // 获取连接对象
    13. //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    14. //connection.flushDb();
    15. //connection.flushAll();
    16. redisTemplate.opsForValue().set("mykey","kuangshen");
    17. System.out.println(redisTemplate.opsForValue().get("mykey"));
    18. }
    19. }
  3. 测试结果此时我们回到Redis查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出:image.png这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。我们转到看那个默认的RedisTemplate内部什么样子:image.png在最开始就能看到几个关于序列化的参数。默认的序列化器是采用JDK序列化器image.png而默认的RedisTemplate中的所有序列化器都是使用这个序列化器:image.png后续我们定制RedisTemplate就可以对其进行修改。RedisSerializer提供了多种序列化方案:

    • 直接调用RedisSerializer的静态方法来返回序列化器,然后setimage.png
    • 自己new 相应的实现类,然后setimage.png
  4. 定制RedisTemplate的模板:

我们创建一个Bean加入容器,就会触发RedisTemplate上的条件注解使默认的RedisTemplate失效。

  1. @Configuration
  2. public class RedisConfig {
  3. @Bean
  4. public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
  5. // 将template 泛型设置为 <String, Object>
  6. RedisTemplate<String, Object> template = new RedisTemplate();
  7. // 连接工厂,不必修改
  8. template.setConnectionFactory(redisConnectionFactory);
  9. /*
  10. * 序列化设置
  11. */
  12. // key、hash的key 采用 String序列化方式
  13. template.setKeySerializer(RedisSerializer.string());
  14. template.setHashKeySerializer(RedisSerializer.string());
  15. // value、hash的value 采用 Jackson 序列化方式
  16. template.setValueSerializer(RedisSerializer.json());
  17. template.setHashValueSerializer(RedisSerializer.json());
  18. template.afterPropertiesSet();
  19. return template;
  20. }
  21. }

这样一来,只要实体类进行了序列化,我们存什么都不会有乱码的担忧了。

缓存穿透与雪崩

缓存穿透

概念
在默认情况下,用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找,数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)缓存都没有命中的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。
解决方案
布隆过滤器
对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。
image.png
缓存空对象
一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。
image.png
这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿(量太大,缓存过期)

概念
相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
解决方案

  1. 设置热点数据永不过期这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
  2. 加互斥锁(分布式锁)在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。

    缓存雪崩

    概念
    大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
    image.png
    解决方案
  • redis高可用这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
  • 限流降级这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  • 数据预热数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

出处:https://blog.csdn.net/weixin_43873227/article/details/106107270