redis分布式锁探讨
基于jedis的能力,探讨了分布式锁的一些初级实现
public class DistributeLock { private JedisCluster jedisCluster; public DistributeLock() { Set<HostAndPort> hostAndPorts = new HashSet<>(); hostAndPorts.add(new HostAndPort("xx.xx.xx.xx", 6379)); jedisCluster = new JedisCluster(hostAndPorts, null, "password"); } /* 方案1:setnx + expire 存在问题: 1、setnx和expire操作两步不是原子操作,如果两步之间服务挂了,或者机器宕机,那么锁将永远无法获取 2、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题 */ boolean getDistributeLock_1(String lockName) { long result = jedisCluster.setnx(lockName, "1"); if (result == 1) { // 加锁成功 jedisCluster.expire(lockName, 30); return true; } // 加锁失败 return false; } /* 方案2:lua脚本合并 setnx + expire 解决问题: 1、通过lua脚本将setnx和expire实现为原子操作 存在问题: 1、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题 */ boolean getDistributeLock_2(String lockName, String value) { String lusScript = "local result1 = redis.call('setnx', KEYS[1], ARGV[2])\n" + "if result1 == 1 then\n" + "redis.call('expire', KEYS[1], ARGV[1])\n" + "return 1\n" + "else return 0\n" + "end;"; Long result = (Long) jedisCluster.eval(lusScript, Arrays.asList(lockName), Arrays.asList("30", value)); return result == 1L; } /* 以上两个方案,都是基于并发量不高的场景,比如分布式的任务。也并没有提供解锁的相关操作, 1、如果锁的ttl设置长了,那其他抢锁的线程就会等待 2、如果锁的ttl设置短了,可能会有并发问题 */ /* 方案3:lua(setnx + expire) + del锁 解决问题: 1、线程执行完了,及时释放锁 存在问题: 1、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题 2、在问题1的基础上,比如线程a先拿到锁,业务实际需要执行35s。30s后ttl失效,线程b抢到锁,5s后,线程a删除锁,a把b的锁删掉了。然后线程c抢到锁,业务混乱,锁失效 */ void doBusiness_3() { String lockName = "wzl_lock"; // 抢锁 boolean result = getDistributeLock_2(lockName, "1"); if (!result) { return; } try { // 业务逻辑 // 1、解放宝岛 // 2、宝岛炒房 // ... } finally { // 解锁 jedisCluster.del(lockName); } } /* 方案4:lua(setnx + expire) + del锁前判断是否是自己的锁 解决问题: 1、解决方案3的问题2(删除别人的锁) 存在问题: 1、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题 2、解锁获取和删除两步是非原子操作 */ void doBusiness_4() { String lockName = "wzl_lock"; String uuid = UUID.randomUUID().toString(); // 抢锁 boolean result = getDistributeLock_2(lockName, uuid); if (!result) { return; } try { // 业务逻辑 // 1、解放宝岛 // 2、宝岛炒房 // ... } finally { // 解锁 if (uuid.equals(jedisCluster.get(lockName))) { jedisCluster.del(lockName); } } } /* 方案5:lua(setnx + expire) + lua(get + del) 解决问题: 1、解决方案4的问题2 存在问题: 1、若超时时间内(30s),业务(38s)无法执行完。锁被释放后,被其他线程获取,业务会出现并发问题 */ void doBusiness_5() { String lockName = "wzl_lock"; String uuid = UUID.randomUUID().toString(); // 抢锁 boolean result = getDistributeLock_2(lockName, uuid); if (!result) { return; } try { // 业务逻辑 // 1、解放宝岛 // 2、宝岛炒房 // ... } finally { // 解锁 getAndDel(lockName, uuid); } } boolean getAndDel(String key, String value) { String lusScript = "local result1 = redis.call('get', KEYS[1])\n" + "if result1 == ARGV[1] then\n" + "redis.call('del', KEYS[1])\n" + "return 1\n" + "else return 0\n" + "end;"; Long result = (Long) jedisCluster.eval(lusScript, Arrays.asList(key), Arrays.asList(value)); return result == 1L; } public static void main(String[] args) { DistributeLock distributeLock = new DistributeLock(); distributeLock.doBusiness_5(); } }
以上实现都不完善,即使是方案5,还是没有解决锁的TTL小于业务真实执行时间,导致的并发问题。
业界对于redis分布式锁的实现,目前所了解的还要数Redisson的实现。
对于方案5的遗留问题,可以通过启动一个看门狗线程,每隔一段时间,查看下锁是否过期,如果没有过期,就重置锁的ttl。
public class RedissonDistributeLock { private RedissonClient redissonClient; public RedissonDistributeLock() { Config config =new Config(); config.useSingleServer().setAddress("redis://xx.xx.xx.xx:6379"); config.useSingleServer().setPassword("password"); redissonClient = Redisson.create(config); } public void doBusiness_6() { String lockName = "wzl_lock"; RLock lock = redissonClient.getLock(lockName); try { // 加锁 lock.lock(); // 业务逻辑 // 1、解放世界无产阶级 // 2、无产阶级万岁 // ... } finally { // 解锁 lock.unlock(); } } }
遗留:redisson分布式锁源码分析