没事儿时候搂B站,看到了诸葛老师讲关于redis的课程,看了看觉得有收获,来记个笔记。
一、redis做分布式锁:
一个简化版的减库存的代码描述:
int stock = jedis.get("p_2");
if (stock > 0) {
stock= stock - 1;
jedis.set("p_2", stock);
}
在单体服务的环境下,当用户数量较多,同时访问这段代码,会出现超卖的情况。库存会变为负数。
考虑加锁:
synchronized {
int stock = jedis.get("p_2");
if (stock > 0) {
stock= stock - 1;
jedis.set("p_2", stock);
}
}
这样可以保证这段的代码的一个顺序访问,但synchronized是JVM级别的,可以理解为只在单个tomcat中有效。
如果现在采用分布式的系统,例如:tomcat集群,dubbo,spring cloud等等,部署了多个减库存的服务,仍然会出现超卖的情况。
这样就需要使用分布式锁了。-- 在分布式系统中,保证共享资源一致性的锁。
redis作为锁:
redis中执行命令的模块是单线程的,可以保证命令的顺序执行
redis中提供了一条命令:setnx -- SET if Not eXists,存储一个 k-v,当且仅当key不存在,如果key存在则不做操作。
使用这条命令就可以对某段代码进行加锁了:
bool = jedis.setnx("key", "value") // 对某个key加锁,例如商品id
if ( !bool)
return;
业务逻辑 ~~~;
jedis.delete("key") // 释放锁
是不是觉得可以了?
如果在执行业务逻辑的时候抛出了异常?
这样锁就无法被释放了,导致其它请求无法访问这段代码。
进行进一步的优化:
bool = jedis.setnx("key", "value") // 对某个key加锁,例如商品id
if ( !bool)
return;
try {
业务逻辑 ~~~;
} finally {
jedis.delete("key") // 释放锁
}
是不是觉得又可以了?
如果在执行业务逻辑的时候服务器宕机了?
这样走不到finally,从而导致锁无法被释放
再优化:
bool = jedis.setnx("key", "value")
jedis.expire("key", 30s) // 设置过期时间,30s之后自动释放锁
if ( !bool)
return;
try {
业务逻辑 ~~~;
} finally {
jedis.delete("key") // 释放锁
}
好的,那么问题又来了
如果在setnx和expire之间服务器宕机了?
再优化,将setnx和expire作为一个原子操作,这里使用redisTemplate
bool = redisTemplate.opsForValue().setIfAbsent("key", "value", 30, "S") // 在setnx的同时,设置过期时间,是一个原子操作,伪代码看懂即可
if ( !bool)
return;
try {
业务逻辑 ~~~;
} finally {
redisTemplate.delete("key") // 释放锁
}
再来看一个问题,直接上图了
锁失效问题:(自己的锁被别人给释放了)
大量的请求导致锁一直失效,
再优化一下:
给每一个请求一个id,删除锁的时候判断id是否为当前请求的id
bool = redisTemplate.opsForValue().setIfAbsent("key", id, 30, "S") // 在setnx的同时,设置过期时间,是一个原子操作,伪代码看懂即可
if ( !bool)
return;
try {
业务逻辑 ~~~;
} finally {
if(id == redisTemplate.opsForValue().get("key"))
redisTemplate.delete("key") // 释放锁
}
问题又来了,
if(id == redisTemplate.opsForValue().get("key"))
在这里出现了卡顿,导致当前锁过期失效,
redisTemplate.delete("key") // 执行到这里就会释放掉其它请求的锁
这样就又导致了锁失效的问题。
锁失效,是锁的过期时间不好控制
当一个锁要到期了,代码仍然没有执行完,那么就需要给这个锁重新设置过期时间
这就是锁续命,为请求执行的主线程中开启一个子线程去定期检测主线程的状态,如果主线程仍然存活,就重新设置主线程锁的时间。
redisson提供的分布式锁就是这样实现的,采用lua脚本控制,保证了操作的原子性
String address = "redis://" + redisHost + ":" + redisPort;
Config redissonConfig = new Config();
// redisson提供了不同redis模式,(哨兵,集群,主从等)这里使用单机模式
redissonConfig.useSingleServer().setAddress(address).setDatabase(1);
Redisson redisson = (Redisson) Redisson.create(redissonConfig);
RLock redissonLock = redisson.getLock("key"); // 根据某个key获取一把锁
try {
redissonLock.lock(); // 上锁
业务逻辑~~
} finally {
redissonLock.unlock(); // 解锁
}
通过以上代码就用redisson实现了分布式锁。
如果锁存储在redis集群中,集群出现主从切换的时候,会导致锁丢失
即使redis进行了持久化也是会存在数据丢失的情况
因此针对锁丢失可以采取一些脚本补偿的策略,
zk也可以提供分布式锁的功能:
针对某一个代码段,只有半数以上的节点加锁成功时才认为给代码段加锁成功。
当master宕机的时候,zk集群会选举数据最新的节点作为master,因此不存在锁丢失的情况,但是zk作为分布式锁的性能较低。
redlock: (流程很繁琐,性能不高,不推荐使用)
redisson也提供了类似于zk形式的分布式锁
需要半数以上的redis加锁成功,才认为代码段加锁成功
分布式系统中的CAP原则:
C:Consistency 一致性
A:Availability 可用性
P:Partition Tolerance 分区容错性
三者不可兼得,zk满足CP, redis满足AP
分段锁:(concurrenthashmap分段思想)
分布式锁还是让同一代码段按顺序执行,分段锁可以让某一个代码段并发执行:
假设有商品100件,编号1-100,
现在分为五组,1~20,21~40,41~60,61~80, 81~100
为五组商品,都加上不同的锁,这样并发数量就由1变为了5了
二、Redis做缓存:
通常的做法是:
读redis,
如果命中直接返回
如果未命中,从mysql中读取,并且写入缓存
更新mysql数据,
删除redis中对应的缓存数据
# 为什么是删除缓存,不是更新缓存?
# 考虑到更新的这个数据不一定会经常的访问,因此没有必要立马进入到缓存。
# 如果这个数据是经常访问的,再从mysql中读取,然后放到缓存中即可,因此没有必要立马将数据更新到缓存当中。(用到的时候再做缓存,是一种lazy的做法)
如果在更新完mysql之后,缓存中的数据删除失败了
那么就会导致redis和mysql中数据不一致的问题,
解决办法是:
先删除redis
然后更新mysql数据
# 这样如果mysql更新失败了,那么redis中还是旧数据,没有出现数据不一致。
由于并发数量比较大,可能会出现如下的情况:
请求A清空缓存 --> 请求B在请求A删除缓存之后读取数据 -->请求B将旧数据从数据库读取到缓存 --> 请求A更新数据库。
如下图所示,这样最终还是会导致redis和mysql中数据不一致。
尝试解决一下这个问题:
1. 读写锁:
如果让所有的请求都按顺序执行,那么就不会出现缓存和数据库中数据不一致的问题。
考虑到对代码段加锁,让代码段顺序执行,这时候使用一般的分布式锁效率就会很低。
考虑使用读写锁,即一个线程在进行写操作的时候不允许其它线程进行读写,
如果一个进行读操作的时候允许其它线程进行读操作但是不允许写,这样在一定程度上可以提高系统的性能。
针对于读多写少的场景下性能比较好。当然了,redisson也提供了分布式的读写锁这里不再给出代码描述了。
2. 延时双删:
先清空redis,
更新mysql
让线程等待一段时间之后,再清空一次redis
# 这样还是会存在数据不一致的情况,如下图
3. 用消息通知的方式去删除缓存:
# 看了这个大佬写的感觉挺好,特别详细,贴一下链接,就不记录了:
https://zhuanlan.zhihu.com/p/59167071
总结一下
看了大佬们写的一些东西还有评论,觉得无论哪种方案都会有争议。
既然使用了缓存,数据不一致性是没办法避免的,使用一些中间件可能会在一定程度上保证数据的一致性,但是会牺牲一些性能。
具体的方案要结合具体的业务来确定。
如果业务没有强一致性的要求,那么也没有必要使用太过复杂的方案,直接给缓存加过期时间就好了,也没有必要延时双删了。
如果有数据的强一致性要求,那直接拿db中的数据做校验也就可以了。
如果mysql做分库分表让业务逻辑变得很复杂,用tidb也是很香的对吧。
这次就记录到这里了!!有啥再补充!!