1、概念
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乐观锁演示实例:
3.4 锁实战
根据一个秒杀案列进行redis锁和事务方面的代码层次的实战。
根据一个个线程访问秒杀实例;没有任何问题;秒杀是基于多个用户同时进行的业务逻辑;所以我们根据apache-jmeter-5.3去模拟多用户同时访问;结果出现了超卖的现象和连接超时的现象。
超时的现象:是多线程同时访问;导致有些线程中途等待;所以超时。
超卖的现象:是多线程同时对一个数据进行操作;导致数据负数的可能。
超时的现象解决方案:
使用连接池解决 == 》
超卖的现象解决方案:
使用事务和锁联合使用解决 ==》
虽然解决了超卖现象的问题;但是由于是乐观锁的机制问题;会导致多个线程同时访问的时候;比对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();
:::
redisTemplate.opsForValue().setIfAbsent(RedisContans.KU_GOODS + ":" + googsId,100); //相当于setnx操作
redisTemplate.opsForValue().setIfAbsent(RedisContans.KU_GOODS + ":" + googsId,100,12, TimeUnit.SECONDS);
//相当于set k1 100 nx ex 12 操作
try {
//声明一个分布式锁
Boolean locks = redisTemplate.opsForValue().setIfAbsent(lock, 100, 3, TimeUnit.SECONDS);
if(!locks){
return "业务繁忙;请稍后;获取锁失败";
}
//对库存数减1
redisTemplate.opsForValue().decrement(RedisContans.KU_GOODS + ":" + googsId);
//保存秒杀用户
redisTemplate.opsForValue().set(RedisContans.USER_GOODS + ":" + googsId,userId);
}finally {
//始终释放锁
//1、直接释放锁 redisTemplate.delete(lock); 会引出误删除其他线程锁
//2、原子性释放锁 利用redis事务特性使加锁和删除锁变成原子性;所谓的原子性就是保证一起被执行;就是要么一起执行成功要么都失败的特性;所以符合redis事务。
redisTemplate.watch(lock); //监视lock锁是否被其他改动;如该lock key有改动;则下面的命令将会执行失败
//开启事务
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
//做删除--释放锁
redisTemplate.delete(lock);
redisTemplate.exec();
}
4.3.2 集群版锁
当业务逻辑处理的时间与redis的lock过期时间不符合的时候;比如;我逻辑处理需要20s;而lock过期时间为10s;这个时候我逻辑代码在处理中;还没做释放锁命令操作;锁就已经丢失了;释放锁必然会报错误;那么使得其他线程也可以进入访问和操作;还是会导致超卖的现象发生;这个时候我们必然需要将这个过期时间做一次延期处理;如何具体去实现呢?另外涉及到redis集群的时候;由于redis的同步slave机制是A分区容错和P高可用是没有保证性的;就是说;slave同步master数据的时候可能会被漏数据风险;那么在集群的时候;就会发生锁丢失的意外;从而导致锁丢失。
解决方案:
针对于以上描述的两大问题;我们采用RedisSession 来解决集群分布式锁;以及如何将快过期的锁续期的问题。
<!-- org.redisson/redisson 分布式专用-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
//单机
RedissonClient redisson = Redisson.create();
Config config = new Config();
config.useSingleServer().setAddress("myredisserver:6379");
RedissonClient redisson = Redisson.create(config);
//主从
Config config = new Config();
config.useMasterSlaveServers()
.setMasterAddress("127.0.0.1:6379")
.addSlaveAddress("127.0.0.1:6389", "127.0.0.1:6332", "127.0.0.1:6419")
.addSlaveAddress("127.0.0.1:6399");
RedissonClient redisson = Redisson.create(config);
//哨兵
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("127.0.0.1:26389", "127.0.0.1:26379")
.addSentinelAddress("127.0.0.1:26319");
RedissonClient redisson = Redisson.create(config);
//集群
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // cluster state scan interval in milliseconds
.addNodeAddress("127.0.0.1:7000", "127.0.0.1:7001")
.addNodeAddress("127.0.0.1:7002");
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 查看当前内存的使用情况。
human 代表目前用户使用占用多少。
maxmemory_human:代表用户可使用多少。
5.2 淘汰策略
一旦数据量超过内存大小;redis也会报OOM;
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的过期时间稀释)