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

没有登录不能评论