目录
1.普通锁举例
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
上面代码,如果单线程肯定没有问题,如果多线程的话,同时扣减库存,有可能出现超卖现象(就是一个东西卖了两遍),就是如果两个人去购买商品的话,假设库存是50,应该剩下48个,但是有可能库存剩余49,这个时候就需要加锁进行处理,如下:
synchronized (this) {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}
如果放到集群或者通过负载均衡打到不同的服务器上面
假设多个线程访问ngnix,ngnix可以分发请求,如果分发到不同tomcat上去,同步代码块只在同一个JVM进程下生效,在分布式场景下synchronized是不起作用的
2.分布式锁实现
初步实现
1. 先设置redis key值
2. 然后使用redis setnx命令,如果key值不存在设置成功,否则失败
3. 获取结果,如果不成功该线程直接返回响应错误码给前端进行提示
4. 如果成功的话,就去仓库里减掉库存
5. 然后删除key,让其他线程可以进来扣减库存
发现问题
1. 如果在执行扣减库存的时候抛出异常,无论如何后面都无法删除掉这个key值
2. 如果服务直接被终止掉或者在catch里面宕机,都运行不到finally
3. 如果正好发生在你setnx和设置过期时间之间宕机
解决方法
一一对应上面的解决办法
1. 用try-finally保证能够将锁释放掉
2. 对key设置过期时间
3. setnx和设置过期时间一起设置,redis会帮你原子控制执行
其他
并发量不大的情况下,上面这种分布式锁已经够用了,但是还有其他情况,就是你业务执行时间过长,假设线程1还在执行过程中,过期时间到了,线程2进来执行,后面线程1又会在finally里面删除key,线程3又可以进来了,这样会反复使得删除,key会一直不生效
主要解决方案就是自己加的锁,自己去释放,别人释放不了
这样的话,线程1执行过程中,线程2进来加锁,线程1执行完之后不会去释放锁,但是依然有可能在第一个线程执行过程中,锁过期第二个线程进来,这样也不好,执行方案如下:
就是在业务代码里面加上定时任务,时刻去注意这个锁的过期时间,如果任务还没有执行结束(就是去判断这个key还存在嘛),如果还存在说明任务没有执行结束,重置过期时间,这样就能保证这个线程执行任务的过程中,是单线程执行的了
3. Redisson分布式锁实现原理
跟上面分布式锁功能全部实现是一样的
3.1 源码分析
1. 获取锁,加锁
RLock redissonLock = redisson.getLock("lock");
redissonLock.lock();
2. lock()方法
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
// 拿到线程id
long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
3. tryAcquire()
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
4. tryAcquireAsync()
leaseTime=-1,走下面那个方法
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture ttlRemainingFuture;
if (leaseTime > 0L) {
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
if (ttlRemaining == null) {
if (leaseTime > 0L) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
this.scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper(f);
}
5. 里面走的是lua脚本
lua脚本大致流程:这边的return nil就是执行完了
1. 判断存不存在这个key,key为getName(),就是锁的名称,说明此时还没有加过锁
2. 如果不存在的话,设置hash类型,key为getName(),field里面key为getLockName(threadId),即线程id,value为1,并且对外层key设置了过期时间,internalLockLeaseTime,这个时间应该是30s
3. 如果存在的话,就说明已经被某个线程加锁了,判断内层value是否为1,如果为1的话,对这个value值进行+1操作,并且设置过期时间,就是重入锁的特征
4. 如果上面两种情况都不是的话,就说明已经被其他线程加锁了,就返回这个key还有多久时间失效,该线程就会一直不断自循环,尝试不断加锁
6. 接着回看tryAcquireAsync()方法
判断这个锁是不是当前线程id设置的锁,如果是当前线程设置的锁那么就重新设置锁过期时间30s,每次执行10s,轮训3次,如果return 0的话,应该会在外面进行判断把看门狗关掉结束
7. 其他线程不断循环
4. 问题
如果当在主节点刚刚加锁成功,主节点挂了,从节点还没有来得及对数据进行同步,也就是从节点还没有这把锁,那以后别的线程过来访问的时候就会加锁成功,这样就会产生两把同样的锁
用zookeeper,强一致性架构,所有节点同步成功之后才会告诉客户端,这把锁加成功了,就算主节点挂了,也会选举肯定有这把锁的节点,但是zookeeper性能没有redis高,其实这个bug其实可以容忍的,如果出现超卖的话,人工去处理
其他处理办法,就是你发送setnx命令,对其他节点也发送这个命令,至少要收到半数以上命令设置成功的响应,才会认为加锁成功,但是这样的话,原来就需要接收一个节点的返回,现在需要收集多个节点的返回,时间增长,如果第一个节点加锁成功,后面加锁过程中失败了,那么第一个是不是要做回滚,要处理的东西太多了
如果处理高并发场景
如果很多线程去抢同一个商品,这个时候你加再多机器也没用,他只访问一个机子,现在这么处理,假设001商品有1000个,我们提高10倍处理速度,将001商品分成001_1到001_10这么10个商品,每个商品对应100个,让这些key落到不同的节点上,相当于水平扩容了,每次随机到一个里面去,减掉库存,如果到0的话,再去其他段查询数据