现在网上有很多基于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、网上有的示例获取锁后,无论获取是否成功,都进行解锁操作。这样可以避免锁一直无法释放的问题,但是又会出现业务未处理完成,锁已经释放了~从而导致了并发问题。