谈及分布式系统的时候,很多企业都会使用到分布式锁,分布式锁的实现方案目前主要是存在两种,redis实现和zookeeper实现。
redis实现方案是多个线程相互竞争,在redis的某一个节点内创建一个键值对,谁能创建成功谁就获取到了锁。锁的释放就是删除当前线程创建的键值对,交给剩余线程去争抢。
/** * 应该有两个时间 一个是redis中的键失效时间 一个是执行是循环的跳出时间 * @param key * @param expire * @return */ @Override public Lock tryLock(String key, int expire, int waitime) { Boolean success; Long start = System.currentTimeMillis(); Long end = System.currentTimeMillis() + (waitime * 1000); Long number = 0L; Lock lock = new Lock(Lockeys+key , UUID.randomUUID().toString() , 0L); while(true){ success = redisTemplate.opsForValue().setIfAbsent(lock.getKey(),lock.getValue(),expire, TimeUnit.SECONDS); if(success == null){ //此处应该抛出异常 获取锁失败 throw new RuntimeException("获取锁失败"); } if(success){ break; } //等待时间太长自动释放 Long curtime = System.currentTimeMillis(); if(curtime>end){ break; } try { Thread.sleep(100); //休眠100毫秒 number++;//也可以根据自旋的次数来选择释放 } catch (InterruptedException e) { e.printStackTrace(); } } Long locktime = System.currentTimeMillis(); lock.setLocktime(locktime-start); lock.setFalg(success); return lock; }
tryLock方法试图去获取锁,获取成功返回true,获取失败返回false。上面的代码主要的封装一个Lock对象,对象中存在的属性是键值对、锁的获取时间等。请求redis的原子操作类去试图创建一个有限时间的键值对。创建成功的线程返回true执行后续的业务逻辑,未获取锁的线程不断在轮询去请求redis试图获取锁,所以线程设置了休眠100毫秒,减少不必要的线程不断的轮询redis。不断轮询的线程不可能一致让它阻塞在此,所以我们这边设置了一个时间,线程等待一定的时间未拿到锁之后,直接返回false。
锁释放:
/** * 为了防止锁释放的不是当前锁,需要校验value * @param lock * @param * @return */ @Override public Boolean unlock(Lock lock) { String value = (String) redisTemplate.opsForValue().get(lock.getKey()); if(value.equals(lock.getValue())){ return redisTemplate.delete(lock.getKey()); }else { return false; } }
锁的释放其实就是在redis中删除当前线程创建的键值对,怎么保证删除对应的键值对是当前线程创建的呢?我们这边比较了一个key值对应的value值,只有两者相同才会执行删除操作。
使用redis的分布式锁绕不开的一个问题就是业务逻辑执行超时后,锁自动释放,所以就无法保证原子性操作。Redis的Liberary中提供了一个第三方的插件Redisson。其大致流程如图所示:
redisson存在一个watch(看门狗)机制,当前业务逻辑执行超时还未释放锁的时候,会定期延长锁的超时时间。
限流对于分布式系统而言是绕不开的问题,redis提供的几种限流的实现方式
(lua脚本实现原子操作)
限流算法:令牌桶算法、漏桶算法
令牌桶算法:存在一个定时任务不断的向桶里面发送令牌,桶里面溢出的话,令牌会丢弃。
一个请求进来会首先去桶中获取令牌,令牌可以获取多个,获取到令牌后才能转发请求,未获取到令牌,请求丢弃。
漏桶算法:
以一定的速率向桶中放置令牌,然后一定的速率流出。突发的大批量请求不适合使用漏桶算法。
lua脚本,通过lua实现redis令牌增加的原子操作
--lua 下标从 1 开始-- 限流 keylocal key = KEYS[1]-- 限流大小local limit = tonumber(ARGV[1])-- 获取当前流量大小local curentLimit = tonumber(redis.call('get', key) or "0")if curentLimit + 1 > limit then -- 达到限流大小 返回 return 0;else -- 没有达到阈值 value + 1 redis.call("INCRBY", key, 1) -- EXPIRE后边的单位是秒 redis.call("EXPIRE", key, 10) return curentLimit + 1end
入参key值和limit限制,如若操作数量则返回0,没有超过限制则Key对应得数量+1。设置key值得过期时间10秒。
我是凯腾凯,互联网浪潮下一枚苟且偷生的程序员