引入分布式锁
redis实现分布式的方式其实有很多,前两种只是单纯介绍实现方式,主要的实践是第三种,前两种实现方式可以直接略过,有兴趣的可以试一试。
大体思路就是设置一个key,这个key是根据业务的需要,来自己定义,比如订单号+ID后几位等等等等,只要唯一就可以。然后设置一个超时时间,这个超时时间是必须设置的,如果发生当前线程出问题的情况,到时间依然可以释放锁,保证其他线程顺利执行,不会造成死锁问题。然后每一个线程进来,判断是否获取锁,获取锁就执行,执行完毕释放锁,没有获取锁就等待获取。
下面介绍几种实现方式
RedisTemplate实现分布式锁
使用RedisTemplate的execute的回调方法,里面使用Setnx方法
Setnx就是,如果没有这个key,那么就set一个key-value, 但是如果这个key已经存在,那么将不会再次设置,get出来的value还是最开始set进去的那个value.
下面直接上代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Component
public class CommonRedisHelper {
//锁名称
public static final String LOCK_PREFIX = "redis_lock";
//加锁失效时间,毫秒
public static final int LOCK_EXPIRE = 300; // ms
@Autowired
RedisTemplate redisTemplate;
/**
*
*
* @param key key值
* @return 是否获取到
*/
public boolean lock(String key){
String lock = LOCK_PREFIX + key;
// 利用lambda表达式
return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
long expireAt = System.currentTimeMillis() + LOCK_EXPIRE + 1;
Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes());
if (acquire) {
return true;
} else {
byte[] value = connection.get(lock.getBytes());
if (Objects.nonNull(value) && value.length > 0) {
long expireTime = Long.parseLong(new String(value));
// 如果锁已经过期
if (expireTime < System.currentTimeMillis()) {
// 重新加锁,防止死锁
byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + LOCK_EXPIRE + 1).getBytes());
return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
}
}
}
return false;
});
}
/**
* 删除锁
*
* @param key
*/
public void delete(String key) {
if(key没有超时){
redisTemplate.delete(key);
}
}
}
业务代码调用
CommonRedisHelper redisHelper = new CommonRedisHelper();
1
boolean lock = redisHelper.lock(key);
if (lock) {
// 执行逻辑操作
redisHelper.delete(key);
} else {
// 设置失败次数计数器, 当到达5次时, 返回失败
int failCount = 1;
while(failCount <= 5){
// 等待100ms重试
try {
Thread.sleep(100l);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (redisHelper.lock(key)){
// 执行逻辑操作
redisHelper.delete(key);
}else{
failCount ++;
}
}
throw new RuntimeException("请稍等再试");
}
阅读代码,其实主要逻辑就是,首先用redis的setNX命令,加入一个key,value为当前时间戳+超时时间+预留时间。setNX命令,如果没有这个key,就加入,返回true。如果存在这个key,就返回false。
如果返回的是false,当前线程没有获取锁,就进入下面的逻辑,通过key,取出value,value为设置好的超时时间的时间戳,然后判断一下是否已经超时,如果超时,就通过getset()方法再次获取锁,并且设置新的value为过期时间戳,返回key的旧值。再判断key的旧值value的时间戳,来判断是否过期,如果过期,则获得锁,如果没有过期,则获取锁失败。
感觉下面这个图,更加直观的展现了加锁的过程
这里有一个问题。为什么要用getset(),不直接set呢。
这里其实牵扯到并发的一些事情,如果直接使用set,那有可能多个客户端会同时获取到锁,如果使用getset然后判断旧值是否过期就不会有这个问题,设想一下如下场景:
1、T1加锁成功,不巧的是,这时C1意外的奔溃了,自然就不会释放锁;
2、T2,T3尝试加锁,这时key已存在,所以T2,T3去判断key是否已过期,这里假设key已经过期了,所以T2,T3使用set指令去设置值,那两个都会加锁成功,这就闯大祸了;如果使用getset指令,然后判断下返回值是否过期就可以避免这种问题,假如T2跑的快,那T3判断返回的时间戳已经过期,自然就加锁失败;
下面分析解锁过程:
直接上图吧
这里又会产生一个问题,既然是释放,直接delete不就完事了?咋还又判断下是否过期呢?
考虑这样一种场景:
1、T1获取锁成功,开始执行自己的操作,不幸的是T1这时被阻塞了;
2、T2这时来获取锁,由于T1被阻塞了很长时间,所以key对应的value已经过期了,这时T2通过getset加锁成功;
3、T1尘封了太久终于被再次唤醒,醒来以后,不判断过期不过期,直接把这个key给删掉;
4、T3来获取锁,在T2还在正常工作的时候,居然一下就成功了,T3也进入了资源;
这样显然不合理吧,出乱子了吧,T2还没干完活呢,T1就把T2的锁给拆了了,T3进来了,这违反了互斥性,对吧。那这里,就加入一个过期时间的判断,那步骤就变成了,T1醒来,一看锁的时间还没到,不管了,让它时间到了自己解锁吧,当超时以后T3也会正常进入。如果T1醒来一看时间到了,就把锁给解开,T3也可以正常进入。
看似安全了,实则还是有漏洞的存在。再设想一下,就还是这个场景,T1醒来,一看锁过期了,就咔的一下子把锁给删了,这个没问题吧?但是这个时候,T2也没有完成任务,也阻塞住了,干活干了一半,但是他依然是要干完后面的活的,这个时候,T3获得锁,开门进来了,T3和T2就一起干活,操作同一片数据,这依然违反了互斥性。
最简单的一种方式