1、初步分布式锁
public R getListLock() {
// 加锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if (lock) {
// 执行业务 ********************************************************
// 查询缓存是否存在
List<CategoryEntity> categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
if (categoryEntities == null) {
// 缓存不存在则查询数据库
categoryEntities = baseMapper.selectList(null);
log.info("查询数据库。。。。。。。。。。。。。");
// 将结果缓存到redis中
redisTemplate.opsForValue().set("tree", categoryEntities);
} else {
log.info("缓存命中。。。。。。。。。。。。。。。");
}
// 执行业务 ********************************************************
// 释放锁
redisTemplate.delete("lock");
return R.ok().put("data", categoryEntities);
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getListLock(); // 自旋重试
}
}
2、优化1:加入执行业务过程中抛异常,导致无法锁无法释放,解决:使用try finally释放锁
public R getListLock() {
// 加锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
List<CategoryEntity> categoryEntities = null;
if (lock) {
try{
// 执行业务 ********************************************************
// 查询缓存是否存在
categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
if (categoryEntities == null) {
// 缓存不存在则查询数据库
categoryEntities = baseMapper.selectList(null);
log.info("查询数据库。。。。。。。。。。。。。");
// 将结果缓存到redis中
redisTemplate.opsForValue().set("tree", categoryEntities);
} else {
log.info("缓存命中。。。。。。。。。。。。。。。");
}
// 执行业务 ********************************************************
} finally {
// 释放锁
redisTemplate.delete("lock");
}
return R.ok().put("data", categoryEntities);
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getListLock(); // 自旋重试
}
}
3、优化2:执行业务过程中服务挂掉,导致无法执行删除key释放锁,解决:给key设置过期时间
public R getListLock() {
// 加锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
redisTemplate.expire("lock", 5, TimeUnit.SECONDS);
List<CategoryEntity> categoryEntities = null;
if (lock) {
try{
// 执行业务 ********************************************************
// 查询缓存是否存在
categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
if (categoryEntities == null) {
// 缓存不存在则查询数据库
categoryEntities = baseMapper.selectList(null);
log.info("查询数据库。。。。。。。。。。。。。");
// 将结果缓存到redis中
redisTemplate.opsForValue().set("tree", categoryEntities);
} else {
log.info("缓存命中。。。。。。。。。。。。。。。");
}
// 执行业务 ********************************************************
} finally {
// 释放锁
redisTemplate.delete("lock");
}
return R.ok().put("data", categoryEntities);
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getListLock(); // 自旋重试
}
}
4、优化3:由于setnx命令和设置key过期命令不是原子性的,此是先set key成功上锁,在这中间如果服务挂掉会导致锁无法释放。解决:set和设置过期时间原子性执行命令
public R getListLock() {
// 加锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1", 5, TimeUnit.SECONDS);
List<CategoryEntity> categoryEntities = null;
if (lock) {
try{
// 执行业务 ********************************************************
// 查询缓存是否存在
categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
if (categoryEntities == null) {
// 缓存不存在则查询数据库
categoryEntities = baseMapper.selectList(null);
log.info("查询数据库。。。。。。。。。。。。。");
// 将结果缓存到redis中
redisTemplate.opsForValue().set("tree", categoryEntities);
} else {
log.info("缓存命中。。。。。。。。。。。。。。。");
}
// 执行业务 ********************************************************
} finally {
// 释放锁
redisTemplate.delete("lock");
}
return R.ok().put("data", categoryEntities);
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getListLock(); // 自旋重试
}
}
5、优化4:由于执行业务代码时候假如执行时间过长,导致redis锁过期了,那么其他线程就可以重新获取锁,并且在释放锁的时候可能会释放其他服务进程上的锁。解决:set key时加一个token标识,删除时候取出来判断是否相等再删除。
public R getListLock() {
// 加锁
String token = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", token, 5, TimeUnit.SECONDS);
List<CategoryEntity> categoryEntities = null;
if (lock) {
try{
// 执行业务 ********************************************************
// 查询缓存是否存在
categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
if (categoryEntities == null) {
// 缓存不存在则查询数据库
categoryEntities = baseMapper.selectList(null);
log.info("查询数据库。。。。。。。。。。。。。");
// 将结果缓存到redis中
redisTemplate.opsForValue().set("tree", categoryEntities);
} else {
log.info("缓存命中。。。。。。。。。。。。。。。");
}
// 执行业务 ********************************************************
} finally {
// 释放锁时先从redis取出锁判断是否相等再删除
String ret = (String) redisTemplate.opsForValue().get("lock");
if (token.equals(ret)) {
redisTemplate.delete("lock");
}
}
return R.ok().put("data", categoryEntities);
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getListLock(); // 自旋重试
}
}
6、优化5:上面判断token是否一致再删除虽然不会出现删错锁的问题,但是由于判断token是否一致,删除key,这两步不是原子性操作,在获取到token时候还是一致的,在进行删锁之前,由于锁过期失效,其他线程进来重新加锁,此时删锁删的就是其他线程加的锁。解决:使用redis脚本执行判断token是否一致并且删锁,保证原子性。
public R getListLock() {
// 加锁
String token = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", token, 5, TimeUnit.SECONDS);
List<CategoryEntity> categoryEntities = null;
if (lock) {
try{
// 执行业务 ********************************************************
// 查询缓存是否存在
categoryEntities = (List<CategoryEntity>) redisTemplate.opsForValue().get("tree");
if (categoryEntities == null) {
// 缓存不存在则查询数据库
categoryEntities = baseMapper.selectList(null);
log.info("查询数据库。。。。。。。。。。。。。");
// 将结果缓存到redis中
redisTemplate.opsForValue().set("tree", categoryEntities);
} else {
log.info("缓存命中。。。。。。。。。。。。。。。");
}
// 执行业务 ********************************************************
} finally {
// 使用Lua脚本保证比较token和删除锁原子性执行
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long ret = (Long) redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), token);
if (ret != null && 1 == ret) {
log.info("成功删除分布式锁。。。。。。。。。。");
}
}
return R.ok().put("data", categoryEntities);
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getListLock(); // 自旋重试
}
}
7、当业务执行过长,分布式锁过期时间到了提前释放了怎么办?
可以通过检查业务是否执行完成来对锁进行续约,可以通过watchdog机制对锁进行续约,前置条件是判断redis key仍然存在,则代表业务还未执行完毕。
更推荐方案:使用redisson,reddsson对以上问题都有考虑了,并且做了实现封装。redission底层对锁续约实现是通过netty的HashedWheelTimer实现的