本文章旨在经验分享,欢迎各位大佬指正
应用场景:1、秒杀
2、抢优惠券
3、接口幂等性校验
总结:抢资源
1、单机锁 --> 到分布式锁1.1、基于opsForValue().setIfAbsent()实现synchronized ()就不再这里做阐述了
格式:setnx key value
将key的值设为value,当且仅当key不存在
若给定的key已存在,则setnx不做任何操作
setnx是【set if not exists】(若果不存在,则set)的简写
可用版本 >= 1.0.0
String lockKey = "lockKey";
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
if(!result){
return "something exception";
}
// 大量业务代码
...
redisTemplate. delete(lockKey);
return "program End ...";
存在的问题,如果业务代码抛异常了,他还能delete吗?
删除不了,就死锁了
try{
String lockKey = "lockKey";
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
if(!result){
return "something exception";
}
// 大量业务代码
...
}
finally{
redisTemplate. delete(lockKey);
}
return "program End ...";
继续看看现在存在的问题 ?
释放锁的问题解决了,但是执行到业务代码宕机了怎么办?finally就没用了,又会出现死锁问题。因为redis里的key始终存在。
try{
String lockKey = "lockKey";
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
// 加超时时间
redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
if(!result){
return "something exception";
}
// 大量业务代码
...
}
finally{
redisTemplate. delete(lockKey);
}
return "program End ...";
但是我们这么写,就咩问题了吗?
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
// 会出现的问题
redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
如果这个key刚刚执行完setIfAbsent(), 结果程序挂了,还没到expire(), 那怎么办??这两行代码有原子性的问题
1.1.3 改进,解决原子性的问题还别说,redis原生就有这两个合二为一的,在底层,redis会帮我们保证原子性
// Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello"); // redisTemplate.expire(lockKey,10,TimeUnit.SECONDS); redisTemplate. opsForValue().setIfAbsent(lockKey, "hello",10,TimeUnit.SECONDS);
这么写的问题是什么呢??
如果放到超高并发场景那就很可怕了,就不小问题了。
假设过来个请求执行了15秒,结果到10秒这个锁就被清理调了,外面又不断有新的请求进来了… …
如此恶性循环下去会有怎样的后果??高并发不断有请求进来... ... 这样会导致什么问题呢?可能这把锁会永久失效!
1.1.4 针对高并发的问题,该怎么解决??本质在于我自己的加的锁,结果会被别的线程给删掉,对不对。
接下来我们能想到的办法就是给这个锁加一个唯一标志。把id加到锁里边去。
String uniqueId = UUID.randomUUID.toString() + threadId;
当我们要解锁的时候判断一下,加锁的id是否是我本人加的
String uniqueId = UUID.randomUUID.toString() + threadId;
try{
String lockKey = "lockKey";
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,uniqueId, "hello",10,TimeUnit.SECONDS);
if(!result){
return "something exception";
}
// 大量业务代码
...
}
finally{
if(uniqueId.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate. delete(lockKey);
}
}
return "program End ...";
当执行到这儿,还是有点问题哈,如果程序执行15秒才结束,但是10秒锁才释放。还是会有并发的问题。
时间不是解决问题的方案,还有没有别的思路?
在redis的处理方式上确实有实现方法:在此现况的情景下,通过定时器去扫,分线程每过10秒扫一次,判断一下主线程是否持有锁,key是否还存在,如果存在,把超时时间再设置长一点,跟token续期类似。
现在市面上都有很多开源框架,来帮我们实现这些问题。而且人家都已经封装好了。
org.redisson redisson 3.6.5
// 初始化客户端
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
// 注入到Spring中
@Bean
public Redisson redisson(){
// 此为单机模式
Config config = new Config();
// 当然,这儿有很多模式可选择,主从、集群、复制、哨兵 等等 ... ...
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
// 业务端
@Autowire
private Redisson redisson;
RLock redissonLock = redisson.getLock(lockkey);
try{
// 执行加锁的操作
redissonLock.lock() // 这行代码可以看成 setIfAbsent(lockKey,uniqueId, "hello",10,TimeUnit.SECONDS);
// 业务代码
... ...
}
finally{
redissonLock.unlock();
}
以下为源码底层加锁逻辑:lua脚本实现
Redisson就不会有问题了吗??
如果redis是做的主从、哨兵,那么依然有问题
主节点写入了key,他是异步的再把数据同步到slave,只要主节点的key写成功了,马上返回给客户端,告诉客户端你这把锁加成功了,key设置成功,客户端线程1开始做业务逻辑了,这个时候,redis主节点才会往从节点去同步。
也就是说:同步在后
在Redis的master节点上拿到了锁;但是这个加锁的key还没有同步到slave节点;master故障,发生故障转移,slave节点升级为master节点;导致锁丢失。
假设,他刚准备同步给从节点,然后主节点挂了,怎么办?假设要同步的这个从节点被选举为master,高并发场景下新的线程(线程3)又进来,他请求的时候发现新的master他没有key,对于这种问题怎么解决呢?
3、Redis主从架构锁失效问题?其实用redis去解决相对麻烦,当然也是有方法的。但是redis我们都已经做到极致了此时可以换种思路,比如用zk,他也是key-value结构,不过是树形的,CAP中满足CP,他不会马上告诉你成功(C:表示一致性),牺牲时间做同步。所以不存在这种问题。
怎么选择redis或者zookeeper,对并发要求比较高还是要选择redis(单机可支持10W的QPS),锁失效也只有手动对数据进行补偿,出错的概率是比较小。
4、RedLock实现redis官方推荐使用 底层实现手段跟zk很类似
4.1、RedLock 简介在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。实现高效的分布式锁有三个属性需要考虑:
安全属性:互斥,不管什么时候,只有一个客户端持有锁效率属性A:不会死锁效率属性B:容错,只要大多数redis节点能够正常工作,客户端端都能获取和释放锁。
Redlock是redis官方提出的实现分布式锁管理器的算法。这个算法会比一般的普通方法更加安全可靠。关于这个算法的讨论可以看下官方文档。
超过半数redis节点(没有任何的依赖关系,就是单独节点)加锁成功才算加锁成功
4.2、RedLock的弊端?性能,原来只要主节点写成功了就行了,现在要很多台机器都加锁成功。都不能100%解决锁失效问题
详细实现可参考 方志朋大佬好文:RedLock的实现



