用分布式锁应该思考的问题:
1、锁的获取和锁的过期时间设置需要原子操作;
2、可重入的问题:判断锁key的对应的value(利用threadlocal的uuid)是否相等;
3、锁可以续期,应该怎么续期?通过一个线程,比较锁key的对应的value(uuid)在一定时间内是发生变化(获取定时查看锁剩余时间,小于10s,重新设置时间,类似watch机制),执行三次即可;
4、删除锁必须当前线程操作,threadlocal获取和存放的value值比较;
5、删除锁之前的 获取比较和删除操作 应该也是原子操作,建议使用lua脚本;
其他详情参考:
Redis实现分布式锁_redis setifabsent-CSDN博客
设计思路:
根据SET resource-name anystring NX EX max-lock-time设置分布式锁,如果上述命令返回OK,那么就可以获得锁(如果返回Nil,那么在一段时间之后重新尝试),从数据库中获取数据操作成功后,通过DEL命令来释放锁。简单实现代码如下:
Boolean isSet = redisTemplate.opsForValue().setIfAbsent("lock",uuid);
if(isSet){
dataFromDb = getDataFromDb();
redisTemplate.delete("lock");//删除锁
}else{
//等待一段时间后重试上述操作,即自旋锁操作
}
}
上述代码会出现三个问题:问题一:执行getDataFromDb()出现异常,那么后续的删除锁操作将无法执行,所以需要将redisTemplate.delete("lock")使用try-catch-finally将异常抛出,并在finally中执行删除锁操作
try{
dataFromDb = getDataFromDb();
}finally {
redisTemplate.delete("lock");//删除锁
}
问题二:如果执行完setIfAbsent()即加锁完成后该进程中断导致后面代码不再执行,那么该锁将永远不会被删除,可以给锁设置一个过期时间避免这种情况。注意加锁与设置过期时间必须同时进行,即必须保证这两步是一个原子操作,不然当加完锁进程中断但过期时间还没设置时还是会出现问题二。加锁与设置过期时间在Spring可以通过setIfAbsent()同时传入锁名和过期时间这两个参数实现。
//加锁的同时设置过期时间,都设在setIfAbsent中可以保证原子性
Boolean isSet = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
问题三:设置了过期时间后,如果获得锁后当前进程还没执行完时过期时间正好到了导致当前进程的锁没了,其他进程占用锁,这样当前进程执行到删除锁步骤时会把其他进程占用的锁给删了。
1) 在删除锁前可以通过value判断锁是不是当前线程的。但是获取value和删除锁两个操作中间如果有时间间隔还是会有可能出现问题三的情况,所以必须保证这两步的原子性。这两步的原子性可以通过lua脚本来实现。
2) 不过上述方法1)解决的是当前线程删除其他线程的锁的情况,没有从根本上解决问题,根本问题是当前线程还在执行,但是执行时锁由于过期被删,其他线程占用锁,这本身就很不合理,没有起到锁的作用,同一时间有两个线程处理业务了。所以要解决这个问题需要通过设置一个守护线程,在当前线程还没执行完业务时且过期时间快到时定期更新过期时间,这样才能防止上述问题。
解决上述三个问题的简单实现的代码如下(守护线程定期更新过期时间没有实现)://实际上通过thredlocal也可以//
/**
* 从数据库获取并封装分类数据
*/
public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁。去Redis占坑
//1.1 setIfAbsent方法等同于SETNX命令
//1.2 注意:加锁和设置过期时间必须是一个原子操作,在调用setIfAbsent传入锁名的同时必须传入过期时间
String uuid = UUID.randomUUID().toString();
Boolean isSet = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if(isSet){
//加锁成功
System.out.println("获取分布式锁成功...");
Map<String, List<Catalog2VO>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
/**
* 获取值对比+对比成功删除 这两步必需是原子操作,
* 不然还是存在获取到lockValue但还没删除时由于时间过期被其他进程拿到锁的可能
* 所以下面这段代码可以使用lua脚本来实现
*/
// String lockValue = redisTemplate.opsForValue().get("lock");
// if(uuid.equals(lockValue)){
// //删除我自己的锁
// redisTemplate.delete("lock");//删除锁
// }
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long deleteFlag = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return dataFromDb;
}else{
//加锁失败..重试
System.out.println("获取分布式锁失败...重试ing");
try{
Thread.sleep(200);
}catch (Exception e){
}
//尽量不要使用递归来进行重试,容易出现栈溢出
return getCatalogJsonFromDbWithLocalLock();
}
}