1、概念

非关系型数据库;以内存为空间;进行数据快速保存。
image.png

2、操作

2.1 常用数据类型

a、List列表
b、String字符串
c、hash
d、set无序列表
e、zset有序列表
Redis支持的五大数据类型包括String(字符串 用法: 键 值),Hash(哈希 类似Java中的 map 用法: 键 键值对),List(列表 用法:键 集合 不可以重复),Set(集合 用法:键 集合 可以重复),Zset(sorted set 有序集合 用法: 键 值(权重分数) 值)

3、事务

3.1 概述

redis事务是一个单独的隔离操作;将事务所涉及的命令都会序列化;并且按顺序执行;中途不会被其他命令请求所中断。

3.2 相关命令

1)Multi :开启事务命令;将涉及的事务命令按顺序组队队列一一敲进去;不会执行。

2)Exec :提交事务命令;统一按顺序执行事务相关命令。

3)discard :放弃该事务。

3.3 悲观锁和乐观锁

这两个机制的锁都是去解决高并发对同一数据操作的问题。
悲观锁:当多个线程去操作一个数据;对这个数据上锁;其他线程去操作时都是阻塞状态;当第一个线程操作完毕;释放锁;允许阻塞线程进行操作;然后循环如此。这就是悲观锁作用。在传统的关系型数据库就用到很多这种锁机制;比如行锁;表锁等、读锁;写锁等;一句话总结悲观锁特性;就是在操作前先上锁。
乐观锁:在多线程操作同一数据的时候;都会携带一个version字段去检查是不是最新的数据.这里有点区分;mysql具体操作是在表里新增version字段;列如:update set xxx where
id = x and version =1.0xx而在redis中;都是根据一个命令来体现的:watch key(某个更新的key);然后进行操作失误;如果两个线程同时进行操作;那么只会一个成功另一个失败。
Redis乐观锁演示实例:
image.png

3.4 锁实战

根据一个秒杀案列进行redis锁和事务方面的代码层次的实战。
image.png
根据一个个线程访问秒杀实例;没有任何问题;秒杀是基于多个用户同时进行的业务逻辑;所以我们根据apache-jmeter-5.3去模拟多用户同时访问;结果出现了超卖的现象和连接超时的现象。
超时的现象:是多线程同时访问;导致有些线程中途等待;所以超时。
超卖的现象:是多线程同时对一个数据进行操作;导致数据负数的可能。
超时的现象解决方案:
使用连接池解决 == 》
image.png

超卖的现象解决方案:
使用事务和锁联合使用解决 ==》
image.png
虽然解决了超卖现象的问题;但是由于是乐观锁的机制问题;会导致多个线程同时访问的时候;比对version是一样的;结果会把多余的线程操作数据失败给没有去秒杀商品;最终会遗留商品数量;对于这样的问题产生;都是源于乐观锁机制导致的。
解决方案:
1、利用LUA脚本执行秒杀代码逻辑。这种的话比较高级;LUA语言一般不熟悉;很难去写。
2、利用RedisSession — 也就是所谓的分布式锁

4、分布式锁

4.1 概述

redis分布式锁是一把锁贯穿多个服务;达到共享锁特点,比如A、B服务区共同操作redis数据进行修改;这个时候事务和乐观锁只能对单体某个服务有效果;对于多个服务间操作就会出问题;所以出了分步式锁机制。

4.2 相关命令

1)setnx :直接set某个key-value;并已开启分布式锁。
setnx k1 100 — 这个就是对k1进行了分布式处理命令;
当再次setnx k1 200的时候就不能够操作了;如何释放锁呢;我们只需要执行
del k1 命令就行了。
2)expire: 给某个key设置过期时间;如果一个锁长时间被某个线程独自占用;就会导致后面的线程造成阻塞;所以我们一般为了解决这一现象的发生;会根据过期时间命令设置。
由于setnx 和expire分两步执行;唯一出现一个宕机产生过期时间未设置成功的现象;所以为了一致性;我们一般使用一键命令:
set k1 100 nx ex 12 :这样写法就是屏蔽了宕机产生过期时间未设置成功的问题。
根据命令解读 nx是开启分布式锁;ex是过期 后面12是时间(单位默认为秒)

4.3 实战

4.3.1 单机版锁

第一将每个进来访问的线程设置一把唯一的锁;
我们可以可以根据以下UUID+线程名称获取唯一锁标识 :::info String lock= UUID.randomUUID()..toString()+Thread.currentThread().getName(); :::

  1. redisTemplate.opsForValue().setIfAbsent(RedisContans.KU_GOODS + ":" + googsId,100); //相当于setnx操作
  2. redisTemplate.opsForValue().setIfAbsent(RedisContans.KU_GOODS + ":" + googsId,100,12, TimeUnit.SECONDS);
  3. //相当于set k1 100 nx ex 12 操作
  4. try {
  5. //声明一个分布式锁
  6. Boolean locks = redisTemplate.opsForValue().setIfAbsent(lock, 100, 3, TimeUnit.SECONDS);
  7. if(!locks){
  8. return "业务繁忙;请稍后;获取锁失败";
  9. }
  10. //对库存数减1
  11. redisTemplate.opsForValue().decrement(RedisContans.KU_GOODS + ":" + googsId);
  12. //保存秒杀用户
  13. redisTemplate.opsForValue().set(RedisContans.USER_GOODS + ":" + googsId,userId);
  14. }finally {
  15. //始终释放锁
  16. //1、直接释放锁 redisTemplate.delete(lock); 会引出误删除其他线程锁
  17. //2、原子性释放锁 利用redis事务特性使加锁和删除锁变成原子性;所谓的原子性就是保证一起被执行;就是要么一起执行成功要么都失败的特性;所以符合redis事务。
  18. redisTemplate.watch(lock); //监视lock锁是否被其他改动;如该lock key有改动;则下面的命令将会执行失败
  19. //开启事务
  20. redisTemplate.setEnableTransactionSupport(true);
  21. redisTemplate.multi();
  22. //做删除--释放锁
  23. redisTemplate.delete(lock);
  24. redisTemplate.exec();
  25. }

4.3.2 集群版锁

当业务逻辑处理的时间与redis的lock过期时间不符合的时候;比如;我逻辑处理需要20s;而lock过期时间为10s;这个时候我逻辑代码在处理中;还没做释放锁命令操作;锁就已经丢失了;释放锁必然会报错误;那么使得其他线程也可以进入访问和操作;还是会导致超卖的现象发生;这个时候我们必然需要将这个过期时间做一次延期处理;如何具体去实现呢?另外涉及到redis集群的时候;由于redis的同步slave机制是A分区容错和P高可用是没有保证性的;就是说;slave同步master数据的时候可能会被漏数据风险;那么在集群的时候;就会发生锁丢失的意外;从而导致锁丢失。
解决方案:
针对于以上描述的两大问题;我们采用RedisSession 来解决集群分布式锁;以及如何将快过期的锁续期的问题。

  1. <!-- org.redisson/redisson 分布式专用-->
  2. <dependency>
  3. <groupId>org.redisson</groupId>
  4. <artifactId>redisson</artifactId>
  5. <version>3.16.0</version>
  6. </dependency>
  7. //单机
  8. RedissonClient redisson = Redisson.create();
  9. Config config = new Config();
  10. config.useSingleServer().setAddress("myredisserver:6379");
  11. RedissonClient redisson = Redisson.create(config);
  12. //主从
  13. Config config = new Config();
  14. config.useMasterSlaveServers()
  15. .setMasterAddress("127.0.0.1:6379")
  16. .addSlaveAddress("127.0.0.1:6389", "127.0.0.1:6332", "127.0.0.1:6419")
  17. .addSlaveAddress("127.0.0.1:6399");
  18. RedissonClient redisson = Redisson.create(config);
  19. //哨兵
  20. Config config = new Config();
  21. config.useSentinelServers()
  22. .setMasterName("mymaster")
  23. .addSentinelAddress("127.0.0.1:26389", "127.0.0.1:26379")
  24. .addSentinelAddress("127.0.0.1:26319");
  25. RedissonClient redisson = Redisson.create(config);
  26. //集群
  27. Config config = new Config();
  28. config.useClusterServers()
  29. .setScanInterval(2000) // cluster state scan interval in milliseconds
  30. .addNodeAddress("127.0.0.1:7000", "127.0.0.1:7001")
  31. .addNodeAddress("127.0.0.1:7002");
  32. RedissonClient redisson = Redisson.create(config);
@Autowired
private Redisson redisson;

//声明一个分布式锁
RLock redissonLock = redisson.getLock(lock);
redissonLock.lock();
      try {
     //对库存数减1
     redisTemplate.opsForValue().decrement(RedisContans.KU_GOODS + ":" + googsId);
    //保存秒杀用户
     redisTemplate.opsForValue().set(RedisContans.USER_GOODS + ":" + googsId,userId);
 }finally {
    //始终释放锁
    redissonLock.unlock();
   //由于超高并发(10000)这种线程涌进来;解锁机制会报一个当前线程非当前线程;不能释放锁的异常。为了避免;我们得加个判断
     if (redissonLock.isLocked()) {  //是否处于被锁状态
       if (redissonLock.isHeldByCurrentThread()) {  //是否是当前加了锁的线程
           redissonLock.unlock();  //释放锁
         }
     }
  }

5、内存满了

5.1 查看

配置文件查看;默认是没有配置;即等于没有限制大小;就是跟随主机内存 .
使用config get (代表所有;可单独查看某个信息) 命令查看;比如查看内存大小:
config get maxmemory 查看内存大小
使用config set (代表某个字段信息) 命令设置某个字段的数值。比如设置内存大小:config set maxmemory 100 查看内存大小;单位默认为字节。
以上是查看配置文件的信息;查看当前的内存具体使用信息;使用以下命令:
info memory 查看当前内存的使用情况。
image.png
human 代表目前用户使用占用多少。
maxmemory_human:代表用户可使用多少。

5.2 淘汰策略

一旦数据量超过内存大小;redis也会报OOM;
image.png
redis默认淘汰策略是noevition;不会驱逐任何key;意思就是满了直接报OOM.
一般呢;我们在生产上采用了allkeys-lru策略;满了;会对所有key进行一个lru算法删除。

6、应用问题

6.1 缓存穿透

问题分析:当在查询大量数据的时候;我们一般对于查询数据进行缓存操作;这样会提高接口的吞吐量;来达到查询快反应;但是某一刻突然大量假请求一下子涌入;reids查询不到数据;然后全去查数据库;导致redis被无视掉了;这就是穿透语义。
解决方案:
1、对于问题分析;无非就是在redis查询不到数据;然后直接过滤掉了;那么我们
2、采用布隆过滤器

6.2 缓存击穿

击穿现象:
1.系统平稳运行过程中;
2.数据库连接量瞬间激增
3.Redis服务器无大量key过期;
4.Redis内存平稳,无波动
5.Redis服务器CPU正常;
6.数据库崩溃;
问题分析:突然间某个热点key过期了;导致大量请求奔向数据库;导致数据库崩溃。
解决方案:
1、加大其key的过期时间;
2、加分布式锁(自动续期),防止被击穿,但是要注意也是性能瓶颈,慎重!
3、启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失

6.3 缓存雪崩

雪崩现象:
1.系统平稳运行过程中,忽然数据库连接量激增
2.应用服务器无法及时处理请求
3.大量408,500错误页面出现
4.客户反复刷新页面获取数据
5.数据库崩溃
6.应用服务器崩溃
7.重启应用服务器无效
8.Redis服务器崩溃
9.Redis集群崩溃
10.重启数据库后再次被瞬间流量放倒
问题分析:1.在一个较短的时间内,缓存中较多的key集中过期
2.此周期内请求访问过期的数据,redis未命中,redis向数据库获取数据
3.数据库同时接收到大量的请求无法及时处理

  • 总结原因:短时间范围内,redis中大量key集中过期;

解决方案:
1、限流、降级
短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问
2、加分布式锁(自动续期)影响性能。
3过期时间使用固定时间+随机值的形式,稀释集中到期的key的数量(将key的过期时间稀释)

6.4 缓存预热

7、持久化