1、为什么需要分布式锁
在微服务系统中,一个请求存在多级跨服务调用,往往需要牺牲强一致性老保证系统高可用,比如通过分布式事务,异步消息等手段完成。但还是有的场景,需要阻塞所有节点的所有线程,对共享资源的访问。比如并发时“超卖”和“余额减为负数”等情况。
分布式锁特性:
- 排他(互斥)性:在任意时刻,只有一个客户端能持有锁。
- 安全性:只有加锁的服务才能有解锁权限。
- 阻塞锁特性:即没有获取到锁,则继续等待获取锁。(根据业务需求)
- 非阻塞锁特性:即没有获取到锁,则直接返回获取锁失败。(根据业务需求)
- 锁失效机制:网络中断或宕机无法释放锁时,锁必须被删除,防止死锁。
- 可重入特性:一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。
分布式锁有哪些实现方法:
目前主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:
- 基于数据库实现(乐观锁 、悲观锁)
- 基于缓存实现(本文只讲非集群Redis)
- 基于ZooKeeper实现
2、使用Redis实现分布式锁
@Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping(value = "/deduct-stock") public String deductSotck() throws Exception { String clientUuid = UUID.randomUUID().toString(); try { // 尝试获取锁,设置超时时间, 根据业务场景估计超时时长 // 使用setIfAbsent保证加锁和设置超时时间是同步的 Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", clientUuid, 10, TimeUnit.SECONDS); // 判断是否获得锁 if (!flag) { return "error"; } int stock = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock")); // 判断库存够不够减 if (stock > 0) { // 将库存回写到redis int tmp = stock - 1; stringRedisTemplate.opsForValue().set("stock", tmp.toString()); logger.info("库存扣减成功"); } else { logger.info("库存扣减失败"); } } finally { // 删除锁的时候判断是不是自己的锁 if (clientUuid.equals(stringRedisTemplate.opsForValue().get("Hello"))) { stringRedisTemplate.delete("Hello"); } } return "end"; }
但是由于程序的不可预知性,谁也不能保证极端情况下,同时会有多个线程同时执行这段业务逻辑。我们可以在当执行业务逻辑的时候同时开一个定时器线程,每隔几秒就重新将这把锁设置为10秒,也就是给这把锁进行“续命”。这样就用担心业务逻辑到底执行多长时间了。但是这样程序的复杂性就会增加,每个业务逻辑都要写好多的代码,因此这里推荐在分布式环境下使用redisson。
3、使用Redisson实现分布式
什么是 Redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
引入依赖:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.6.5</version> </dependency>
初始化Redisson的客户端配置:
@Bean public Redisson redisson () { Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0); return (Redisson) Redisson.create(config); }
代码实现:
@Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private Redisson redisson; @RequestMapping(value = "/deduct-stock") public String deductSotck() throws Exception { // 获取锁对象 RLock lock = redisson.getLock("Hello"); try { // 尝试加锁, 默认30秒, 自动后台开一个线程实现锁的续命 lock.tryLock(); int stock = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock")); // 判断库存够不够减 if (stock > 0) { // 将库存回写到redis int tmp = stock - 1; stringRedisTemplate.opsForValue().set("stock", tmp.toString()); logger.info("库存扣减成功"); } else { logger.info("库存扣减失败"); } } finally { // 释放锁 lock.unlock(); } return "end"; }
Redisson分布式锁的实现原理如下:
1.通过 getLock 方法获取对象 org.redisson.Redisson#getLock()
@Override public RLock getLock(String name) { /** * 构造并返回一个 RedissonLock 对象 * commandExecutor: 与 Redis 节点通信并发送指令的真正实现。需要说明一下,CommandExecutor 实现是通过 eval 命令来执行 Lua 脚本 * name: 锁的全局名称 * id: Redisson 客户端唯一标识,实际上就是一个 UUID.randomUUID() */ return new RedissonLock(commandExecutor, name, id); }
2.通过tryLock方法尝试获取锁,org.redisson.RedissonLock#tryLock:
@Override public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException { //取得最大等待时间 long time = unit.toMillis(waitTime); //记录下当前时间 long current = System.currentTimeMillis(); //取得当前线程id(判断是否可重入锁的关键) long threadId = Thread.currentThread().getId(); //1.尝试申请锁,返回还剩余的锁过期时间 Long ttl = tryAcquire(leaseTime, unit, threadId); //2.如果为空,表示申请锁成功 if (ttl == null) { return true; } //3.申请锁的耗时如果大于等于最大等待时间,则申请锁失败 time -= System.currentTimeMillis() - current; if (time <= 0) { /** * 通过 promise.trySuccess 设置异步执行的结果为null * Promise从Uncompleted-->Completed ,通知 Future 异步执行已完成 */ acquireFailed(threadId); return false; } current = System.currentTimeMillis(); /** * 4.订阅锁释放事件,并通过await方法阻塞等待锁释放,有效的解决了无效的锁申请浪费资源的问题: * 基于信息量,当锁被其它资源占用时,当前线程通过 Redis 的 channel 订阅锁的释放事件,一旦锁释放会发消息通知待等待的线程进行竞争 * 当 this.await返回false,说明等待时间已经超出获取锁最大等待时间,取消订阅并返回获取锁失败 * 当 this.await返回true,进入循环尝试获取锁 */ RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId); //await 方法内部是用CountDownLatch来实现阻塞,获取subscribe异步执行的结果(应用了Netty 的 Future) if (!await(subscribeFuture, time, TimeUnit.MILLISECONDS)) { if (!subscribeFuture.cancel(false)) { subscribeFuture.onComplete((res, e) -> { if (e == null) { unsubscribe(subscribeFuture, threadId); } }); } acquireFailed(threadId); return false; } try { //计算获取锁的总耗时,如果大于等于最大等待时间,则获取锁失败 time -= System.currentTimeMillis() - current; if (time <= 0) { acquireFailed(threadId); return false; } /** * 5.收到锁释放的信号后,在最大等待时间之内,循环一次接着一次的尝试获取锁 * 获取锁成功,则立马返回true, * 若在最大等待时间之内还没获取到锁,则认为获取锁失败,返回false结束循环 */ while (true) { long currentTime = System.currentTimeMillis(); // 再次尝试申请锁 ttl = tryAcquire(leaseTime, unit, threadId); // 成功获取锁则直接返回true结束循环 if (ttl == null) { return true; } //超过最大等待时间则返回false结束循环,获取锁失败 time -= System.currentTimeMillis() - currentTime; if (time <= 0) { acquireFailed(threadId); return false; } /** * 6.阻塞等待锁(通过信号量(共享锁)阻塞,等待解锁消息): */ currentTime = System.currentTimeMillis(); if (ttl >= 0 && ttl < time) { //如果剩余时间(ttl)小于wait time ,就在 ttl 时间内,从Entry的信号量获取一个许可(除非被中断或者一直没有可用的许可)。 getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } else { //则就在wait time 时间范围内等待可以通过信号量 getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS); } //7.更新剩余的等待时间(最大等待时间-已经消耗的阻塞时间) time -= System.currentTimeMillis() - currentTime; if (time <= 0) { acquireFailed(threadId); return false; } } } finally { //7.无论是否获得锁,都要取消订阅解锁消息 unsubscribe(subscribeFuture, threadId); } }
其中 tryAcquire 内部通过调用 tryLockInnerAsync 实现申请锁的逻辑。申请锁并返回锁有效期还剩余的时间,如果为空说明锁未被其它线程申请则直接获取并返回,如果获取到时间,则进入等待竞争逻辑。
3、org.redisson.RedissonLock#tryLockInnerAsync 流程图:
实现源码:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { internalLockLeaseTime = unit.toMillis(leaseTime); /** * 通过 EVAL 命令执行 Lua 脚本获取锁,保证了原子性 */ return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command, // 1.如果缓存中的key不存在,则执行 hset 命令(hset key UUID+threadId 1),然后通过 pexpire 命令设置锁的过期时间(即锁的租约时间) // 返回空值 nil ,表示获取锁成功 "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + // 如果key已经存在,并且value也匹配,表示是当前线程持有的锁,则执行 hincrby 命令,重入次数加1,并且设置失效时间 "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + //如果key已经存在,但是value不匹配,说明锁已经被其他线程持有,通过 pttl 命令获取锁的剩余存活时间并返回,至此获取锁失败 "return redis.call('pttl', KEYS[1]);", //这三个参数分别对应KEYS[1],ARGV[1]和ARGV[2] Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); }
参数说明:
KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key;
ARGV[1]就是internalLockLeaseTime,即锁的租约时间(持有锁的有效时间),默认30s;
ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值 value,即UUID+threadId。
解锁源码分析
unlock 内部通过 get(unlockAsync(Thread.currentThread().getId())) 调用 unlockInnerAsync 解锁。
@Override public void unlock() { try { get(unlockAsync(Thread.currentThread().getId())); } catch (RedisException e) { if (e.getCause() instanceof IllegalMonitorStateException) { throw (IllegalMonitorStateException) e.getCause(); } else { throw e; } } }
get方法利用是 CountDownLatch 在异步调用结果返回前将当前线程阻塞,然后通过 Netty 的 FutureListener 在异步调用完成后解除阻塞,并返回调用结果。
org.redisson.RedissonLock#unlockInnerAsync 流程图:
4、使用ConcurrentHashMap实现内部锁
/** * 业务锁 */ public class LockMapService { // syncGoods锁 private static Map<String, Long> syncGoodsMap = new ConcurrentHashMap<>(); public static Map<String, Long> getSyncGoodsMap() { return syncGoodsMap ; } public static void setSyncGoodsMap(Map<String, Long> syncGoodsMap) { LockMapService.syncGoodsMap = syncGoodsMap; } public static void syncGoodsMapRemove(String key) { synchronized (syncGoodsMap) { if(syncGoodsMap.get(key) != null) { syncGoodsMap.remove(key); } } } }
使用锁:
// 使用业务锁 public void methodLock(){ String key = "lock:ssk:id:" + user.getId(); if(LockMapService.getSyncGoodsMap().get(key) != null) { throw new TaskExecuteException(); } try { LockMapService.getSyncGoodsMap().put(key, System.currentTimeMillis()); // 业务处理 }finally{ LockMapService.syncGoodsMapRemove(key); } }