Redis分布式锁
实现源码:
https://github.com/huangzhenshi/DistributeLearning/tree/master/DistributeLearning-master
实现方式
乐观锁、悲观锁的方式
- 乐观锁实现原理:通过watch存货量变量,先做第一轮判断,存货大于0,开启事务,减库存,然后提交事务,如果存货发生变动,就操作失效,循环操作直到成功为止,或者是存量为负public void run() {while (true) {System.out.println("顾客:" + clientName + "开始抢商品");jedis = RedisUtil.getInstance().getJedis();try {jedis.watch(key);int prdNum = Integer.parseInt(jedis.get(key));// 当前商品个数if (prdNum > 0) {Transaction transaction = jedis.multi();transaction.set(key, String.valueOf(prdNum - 1));List<Object> result = transaction.exec();if (result == null || result.isEmpty()) {System.out.println("悲剧了,顾客:" + clientName + "没有抢到商品");// 可能是watch-key被外部修改,或者是数据操作被驳回} else {jedis.sadd(clientList, clientName);// 抢到商品记录一下System.out.println("好高兴,顾客:" + clientName + "抢到商品");System.out.println("account is: "+ prdNum);break;}} else {System.out.println("悲剧了,库存为0,顾客:" + clientName + "没有抢到商品");break;}} catch (Exception e) {e.printStackTrace();} finally {jedis.unwatch();RedisUtil.returnResource(jedis);}}}
悲观锁的实现
通过redis的setnx方法,多个线程对同一个key操作,一次只有一个线程会成功,来实现锁操作,解锁通过jedis.del(lockKey)。另外引入expireTime的参数,来解决异常线程占用锁,使该方法更健壮。
- 类似于Zookeeper的实现原理,需要一个辅助类RedisBasedDistributedLock来实现 tryLock和lock、unlock等操作
- 但是这里的逻辑是非公平锁的形式去抢锁,每个用户循环操作,先读取存量,如果大于0就有3s的时间去抢锁,抢锁失败然后进入下一个循环,这样设计防止库存为0了,其它未抢到锁的线程还不知道。
- 抢锁成功的话,再校验一次存货,仍然大于0,就消费一个库存,添加消费者,最后释放锁,结束循环
- 引入了锁超时机制,即使当前线程出现异常,未成功释放锁,也不会让当前业务永远停摆
- 抢锁的过程也为在超时时限内循环抢锁,而且是抢2次,第一次通过jedis.setnx(lockKey, stringOfLockExpireTime) == 1来抢,这是主要的获取锁的方式
- 如果正常获取不到锁,再通过String oldValue = jedis.getSet(lockKey, stringOfLockExpireTime);来判断当前拥有锁的线程是否超时了,因为是原子操作,如果超时了,只有1个线程可以获取到超时的oldValue
- 释放锁的逻辑也很暴力,直接删除: jedis.del(lockKey);
- 其实也可以通过 jedis.setnx(lockKey,Expire,lockName)的形式实现自动的超时判断,而不需要额外的代码进行判断,更简洁锁的过期时间是5s,初始化的时候设置了public void init() {jedis = RedisUtil.getInstance().getJedis();redisBasedDistributedLock = new RedisBasedDistributedLock(jedis, "lock.lock", 5 * 1000);}public void run() {while (true) {if(Integer.valueOf(jedis.get(key))<= 0) {break;}//缓存还有商品,取锁,商品数目减去1System.out.println("顾客:" + clientName + "开始抢商品");if (redisBasedDistributedLock.tryLock(3,TimeUnit.SECONDS)) { //等待3秒获取锁,否则返回falseint prdNum = Integer.valueOf(jedis.get(key)); //再次取得商品缓存数目if (prdNum > 0) {jedis.decr(key);//商品数减1jedis.sadd(clientList, clientName);// 抢到商品记录一下System.out.println("好高兴,顾客:" + clientName + "抢到商品");} else {System.out.println("悲剧了,库存为0,顾客:" + clientName + "没有抢到商品");}redisBasedDistributedLock.unlock();break;}}//释放资源redisBasedDistributedLock = null;RedisUtil.returnResource(jedis);}
lock()
- 设置3s的最长抢锁时间,也是循环抢锁
- 引入了锁超时机制,即使当前线程出现异常,未成功释放锁,也不会让当前业务永远停摆
- 获取锁成功,会设置locked = true;和setExclusiveOwnerThread(Thread.currentThread());lock(true, time, unit, false);// 阻塞式获取锁的实现protected boolean lock(boolean useTimeout, long time, TimeUnit unit, boolean interrupt) throws InterruptedException {System.out.println("test1");if (interrupt) {checkInterruption();}System.out.println("test2");long start = System.currentTimeMillis();long timeout = unit.toMillis(time); // if !useTimeout, then it's uselesswhile (useTimeout ? isTimeout(start, timeout) : true) {System.out.println("test3");if (interrupt) {checkInterruption();}long lockExpireTime = System.currentTimeMillis() + lockExpires + 1;// 锁超时时间String stringOfLockExpireTime = String.valueOf(lockExpireTime);System.out.println("test4");if (jedis.setnx(lockKey, stringOfLockExpireTime) == 1) { // 获取到锁System.out.println("test5");//成功获取到锁, 设置相关标识locked = true;setExclusiveOwnerThread(Thread.currentThread());return true;}System.out.println("test6");String value = jedis.get(lockKey);if (value != null && isTimeExpired(value)) { // lock is expiredSystem.out.println("test7");// 假设多个线程(非单jvm)同时走到这里String oldValue = jedis.getSet(lockKey, stringOfLockExpireTime); //原子操作// 但是走到这里时每个线程拿到的oldValue肯定不可能一样(因为getset是原子性的)// 加入拿到的oldValue依然是expired的,那么就说明拿到锁了System.out.println("test8");if (oldValue != null && isTimeExpired(oldValue)) {System.out.println("test9");//成功获取到锁, 设置相关标识locked = true;setExclusiveOwnerThread(Thread.currentThread());return true;}} else {// TODO lock is not expired, enter next loop retrying}}System.out.println("test10");return false;}
lock的优化方案:不通过去isTimeExpired判断持锁线程是否过期,而是获取到锁的时候,就自动设置过期时间,来降低CPU的消耗