redis分布式锁
基本原理
本地锁只能锁住一台机器上的单例线程,在分布式系统中,有多少台机器,就会放进多少个查询线程。因此,在分布式系统中,需要使用分布式锁。
分布式基本原理:所有的机器去同一个地方“占坑”,如果占到,就执行逻辑,否则就必须等待,直到释放锁。“占坑”可以去redis
,可以去数据库,可以在所有机器都能访问的地方,等待则使用自旋的方式。
分布式锁演进-阶段一
// 1.占分布式锁。即在redis里面进行“占坑”
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
// 2.拿到“占坑”锁的机器线程去执行业务,其他的线程则进入等待
if (lock) {
// 拿到锁的线程进入开始执行业务
......
// 业务执行完成,删除redis的锁,让其他等待线程可以拿锁执行业务
redisTemplate.delete("lock");
} else {
// 没有拿到锁的线程,开始进行自旋等待
// 休眠100ms重新调用本方法
}
可能出现的问题:
拿到锁的线程在执行业务的过程中抛了异常或者机器宕机,后面执行锁的业务没有执行,redis
中的锁一直没有删除,成了死锁,后续所有等待的线程都拿不到锁执行不了业务了。
解决方法:
设置锁的过期时间,即使没有删除,也会自动删除。
分布式锁演进-阶段二
// 1.占分布式锁。即在redis里面进行“占坑”
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
// 2.拿到“占坑”锁的机器线程去执行业务,其他的线程则进入等待
if (lock) {
// 设置过期时间
redisTemplate.expire("lock", 30, Ti,eUnit.SECONDS);
// 拿到锁的线程进入开始执行业务
......
// 业务执行完成,删除redis的锁,让其他等待线程可以拿锁执行业务
redisTemplate.delete("lock");
} else {
// 没有拿到锁的线程,开始进行自旋等待
// 休眠100ms重新调用本方法
}
可能出现的问题:
setnx
设置好,去设置锁过期时间时,又宕机了,又会出现死锁问题。
解决方法:
设置过期时间和“占坑”是原子的,redis
支持使用setnx ex
命令。
分布式锁演进-阶段三
// 1.占分布式锁。即在redis里面进行“占坑”
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1", 300, TimeUnit.SECONDS);
// 2.拿到“占坑”锁的机器线程去执行业务,其他的线程则进入等待
if (lock) {
// 拿到锁的线程进入开始执行业务
......
// 业务执行完成,删除redis的锁,让其他等待线程可以拿锁执行业务
redisTemplate.delete("lock");
} else {
// 没有拿到锁的线程,开始进行自旋等待
// 休眠100ms重新调用本方法
}
可能出现的问题:
删除别人的锁。由于执行业务的时间过长,锁在途中自己过期了,过期之后下一个线程就可以拿锁开始执行业务,这时候,上一个线程业务执行完了,此时删除锁,删除的就是下一个线程的锁。
为每一把锁执行uuid
,删除的时候匹配的锁才能删除。
分布式锁演进-阶段四
可能出现的问题:
如果判断正好是当前锁的之后,正要执行下一步删除锁操作时,锁自动过期了,别的线程已经设置了新的值,那么删除的还是别人的锁。
解决方法:
比对锁与删除锁必须保证是原子操作。使用官方提供的redis + lua
脚本完成。
参考:http://redis.cn/commands/set.html
分布式锁演进-终
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
String uuid = UUID.random().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
Boolean lock = ops.setIfAbsent("lock", uuid, 500, TimeUnit.SECONDS);
if (lock) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();
String lockValue = ops.get("lock");
// get和delete原子操作
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
stringRedisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class), // 脚本和返回类型
Arrays.asList("lock"), // 参数
lockValue); // 参数值,锁的值
return categoriesDb;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 睡眠0.1s后,重新调用
return getCatalogJsonDbWithRedisLock();
}
}