基于redis实现分布式锁

基于redis实现分布式锁

缓存锁

  开始接触缓存服务,知道很多应用都把缓存作为分布式锁,比如redis。使用缓存作为分布式锁,性能非常强劲,在一些不错的硬件上,redis可以每秒执行10w次,内网延迟不超过1ms,足够满足绝大部分应用的锁定需求。

   redis锁的原理是利用setnx命令,即只有在某个key不存在情况才能set成功该key,这样就达到了多个进程并发去set同一个key,只有一个进程能set成功。

   redis自带的expire功能可以不需要应用主动去删除锁。而且从 Redis 2.6.12 版本开始,redis的set命令直接直接设置NX和EX属性,NX即附带了setnx数据,key存在就无法插入,EX是过期属性,可以设置过期时间。这样一个命令就能原子的完成加锁和设置过期时间。

   缓存锁优势是性能出色,劣势就是由于数据在内存中,一旦缓存服务宕机,锁数据就丢失了。像redis自带复制功能,可以对数据可靠性有一定的保证,但是由于复制也是异步完成的,因此依然可能出现master节点写入锁数据而未同步到slave节点的时候宕机,锁数据丢失问题。

代码实现

public class RedisLock {
	private static Random random = new Random();

	/**
	 * Lock key path.
	 */
	private String lockKey;

	/**
	 * 锁超时时间,防止线程在入锁以后,无限的执行等待
	 */
	private int expireMsecs = 60 * 1000;

	/**
	 * 锁等待时间,防止线程饥饿
	 */
	private int timeoutMsecs = 10 * 1000;

	private volatile boolean locked = false;

	/**
	 * Detailed constructor with default acquire timeout 10000 msecs and lock
	 * expiration of 60000 msecs.
	 *
	 * @param lockKey
	 *            lock key (ex. account:1, ...)
	 */
	public RedisLock(String lockKey) {
		this.lockKey = lockKey + "_lock";
	}

	/**
	 * @return lock key
	 */
	public String getLockKey() {
		return lockKey;
	}

	/**
	 * 获得 lock. 实现思路: 主要是使用了redis 的setnx命令,缓存了锁. reids缓存的key是锁的key,所有的共享,
	 * value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间) 执行过程:
	 * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
	 * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
	 *
	 * @return true if lock is acquired, false acquire timeouted
	 * @throws InterruptedException
	 *             in case of thread interruption
	 */
	public synchronized boolean tryLock() throws InterruptedException {
		int timeout = timeoutMsecs;
		while (timeout >= 0) {
			long expires = System.currentTimeMillis() + expireMsecs + 1;
			String expiresStr = String.valueOf(expires); // 锁到期时间
			if (SingleJedisUtil.getInstance().STRINGS.setnx(lockKey, expiresStr) == 1) {
                //如果执行完任务,恰好解锁失败,设置过期时间防止锁不释放
                  SingleJedisUtil.getInstance().KEYS.expired(lockKey, 60);//过期时间一分钟
				locked = true;
				return true;
			}

			String currentValueStr = SingleJedisUtil.getInstance().STRINGS.get(lockKey); // redis里的时间
			// 判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
			if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
				// lock is expired
				String oldValueStr = SingleJedisUtil.getInstance().STRINGS.getSet(lockKey, expiresStr);
				// 获取上一个锁到期时间,并设置现在的锁到期时间,
				// 只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
				if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
					// 防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
					// [分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
					// lock acquired
					locked = true;
					return true;
				}
			}
			
			int sleepTime = random.nextInt(200);
			timeout -= sleepTime;

			/*
			 * 延迟100 毫秒, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
			 * 只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
			 * 使用随机的等待时间可以一定程度上保证公平性
			 */
			Thread.sleep(sleepTime);
		}
		return false;
	}

	/**
	 * Acqurired lock release..
	 */
	public synchronized void unlock() {
		if (locked) {
			SingleJedisUtil.getInstance().KEYS.del(lockKey);
			locked = false;
		}
	}

   这里解释下while (timeout >= 0) ,如果当前线程拿不到锁,进行自旋,不断尝试获取锁,取锁时间大于超时等待时间,直接返回false,获取锁失败。

总结

  根据业务的场景、现状以及已经依赖的服务,应用可以使用不同分布式锁实现。不过我个人觉得,如果需要最可靠的分布式锁,还是使用zookeeper会更可靠些。curator-recipes库封装的分布式锁,java应用也可以直接使用。而且如果开始依赖zookeeper,那么zookeeper不仅仅提供了分布式锁功能,选主、服务注册与发现、保存元数据信息等功能都能依赖zookeeper,这让zookeeper不会那么闲置, 当然如果项目没有依赖zookeeper,还是用redis吧。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值