现在网上有很多基于Redis实现分布式锁的文章,但是这里坑也很多,后面我会逐一介绍。
首页我先贴上代码,之后再进行逐一说明。
/** * 应该以: lock(); try { doSomething(); } finally { unlock(); } 的方式调用 * * 2016年6月13日 下午2:51:01 flyfox 369191470@qq.com */ public class RedisLock { /** 加锁标志 */ public static final String LOCK_VALUE = "TRUE"; /** * redis客户端封装 */ private JedisClient jedis; private String key; // 锁状态标志 private boolean locked = false; /** 默认超时时间(毫秒) */ private long timeout = 50 * 1000; /** 锁的超时时间(秒),过期删除 */ private int expire = 10; /** * This creates a RedisLock * * @param key */ public RedisLock(String key) { this.jedis = JedisClient.getInstance(); this.key = key + "_lock"; } /** * This creates a RedisLock * * @param key * @param timeout */ public RedisLock(String key, long timeout) { this.jedis = JedisClient.getInstance(); this.key = key + "_lock"; this.timeout = timeout; } /** * This creates a RedisLock * * @param key * @param timeout * @param expire */ public RedisLock(String key, long timeout, int expire) { this.jedis = JedisClient.getInstance(); this.key = key + "_lock"; this.timeout = timeout; this.expire = expire; } /** * 加锁 * */ public boolean lock() { long start = System.currentTimeMillis(); try { while ((System.currentTimeMillis() - start) < timeout) { if (this.jedis.setnx(this.key, LOCK_VALUE) == 1) { this.jedis.expire(this.key, this.expire); this.locked = true; return this.locked; } // 短暂休眠,避免出现活锁 Thread.sleep(500L); } } catch (Exception e) { throw new RuntimeException("Locking error", e); } return false; } /** * 解锁 */ public void unlock() { this.jedis.del(this.key); } /** * 未获取锁,删除异常锁 */ public boolean tryUnlock() { boolean flag = false; // 根据ttl,如果异常或者没有设置,删除锁 long ttl = this.jedis.ttl(this.key); if (ttl == -1 || ttl > this.expire) { this.jedis.del(this.key); flag = true; } return flag; } }
调用示例:
/** * 测试 */ public class RedisLockTest { public static void main(String[] args) { RedisLock lock = new RedisLock("test"); boolean flag = false; try { // 尝试获取锁 flag = lock.lock(); if (flag) { // 业务代码 doSomething(); } } finally { // 获取锁成功,业务处理完成,释放锁 if (flag) { lock.unlock(); } else { // 未获取到锁,尝试解除异常锁 lock.tryUnlock(); // 如果解除成功,应该打印异常日志 } } } /** * 业务调用 * * 2016年6月13日 下午2:56:31 flyfox 369191470@qq.com */ private static void doSomething() { // TODO Auto-generated method stub } }
这里面有许多需要注意的点。首先JedisClient是Jedis的封装,也可以理解为Jedis。
1、setnx方法:如果字段是个新的字段,并成功赋值,返回1;如果字段已经存在,返回0。我们这里主要通过setnx实现锁机制。get和set命令也能实现setnx方法,但是由于命令多次调用,就不是原子级了,setnx是redis内部实现的原子级命令。
2、expire方法:由于考虑到,服务宕机或者被kill导致锁无法释放,这个加入了expire设置,这样可以有效的避免宕机后锁一直未释放问题。网上很多都说到了这一点。但是在大量请求的时候,可能就正好只调用了setnx,而没调用到expire,这是很正常的,这样也一样会导致锁被占用而无法释放的问题。解决方法我们后面会介绍。
3、timeout设置:这里设置的是当前请求的锁,获取等待时间。如果超时,放弃锁获取,结束执行逻辑。
4、如果获取到了锁,那么业务逻辑执行完成后,应该主动释放锁。这里放到了finally里面主要是避免业务处理异常,锁始终无法释放问题。
5、最后是加入了如果未获取到锁,进行尝试解锁处理。由于考虑到上面说到的宕机或者kill掉后,setnx执行但是expire未执行情况,这里对ttl进行了判断。如果未设置或者超时时间异常,那么我们主动释放掉锁。这里我是把tryUnlock()放到了最后,因为这是异常情况,如果出现应该打印异常日志。当然我们也可以优化下,在lock.lock();返回false时,调用tryUnlock()尝试解锁,如果解锁成功,再进行lock()获取,这样能避免当前请求未做任何处理的情况。
6、这里面还有一个细节就是如果expire时间内,业务未处理完成。这样锁已经自动释放,那么我们最后unlock()操作解锁的就是其他人获取到的锁了。这种情况比较特殊,我们要特别注意expire的设置,应该设置expire是我们当前请求能接受的最大时间,尽量避免expire超时。
7、网上有的示例获取锁后,无论获取是否成功,都进行解锁操作。这样可以避免锁一直无法释放的问题,但是又会出现业务未处理完成,锁已经释放了~从而导致了并发问题。