以下内容纯粹是个人在闲聊没事时凭自己的理解总结出来的,全部为手敲,可能在内容方面和网上的一些博客不一样,没有那些博主总结的到位,这里只是为了一个记录和总结。
1、概念
一个方法或者一段代码,在分布式情况,同一时间内只能被一个台机器执行。
2、特征
- 锁的有效释放,防止死锁;
- 可重入锁;
- 高可用性;
- 高性能获取锁和释放锁;
- 非阻塞锁;
3、实现方案
3.1 基于数据库
在数据库中创建一个表,字段大概为:id,methodName(设置唯一字段),version。
利用数据库的字段唯一特性,获取到锁后再数据库中创建一条记录,如果创建失败,则为获取锁失败,可以休息一段时间再次尝试获取锁。
问题:
1.锁的释放,如果程序在释放锁之前,宕机 则一直会占用锁 解决办法:可以写一个定时任务,定时清除数据库中数据,定时器时间是个问题
2.可重入锁:可以利用version字段设置一个字段,判断试过当前线程持有的version和数据库中version一致,则可以继续使用当前锁
3.高可用:数据库要搭建高可用集群,如果单节点数据库,则数据库挂了就。。。。。
3.2 基于Redis
利用Redis的setNx命令,set if not exists 如果不存在,执行setnx命令,如果存在,则什么也不做,直接返回。
setNx(key,value,time,timeunit);
key:锁的唯一key
value:这里值可以为一个uuid,每一个客户端都有一个唯一的uuid,然后通过比较判断可以实现可重入锁
time:过期时间,如果程序宕机,则会在超时后自动释放锁,防止死锁的出现
timeunit:时间单位
问题:
1、如果此处设置的时间过短,那么程序还没有执行完,redis就自动释放了锁,会引发并发问题。解决方法:可以设置一个线程,去个锁"续命"
2、高可用:redis服务器必须是高可用的集群
3、setNx中的value要唯一,在释放锁时要进行判断,必须是自己创建的锁才可以释放
在工作中,我们一般使用Redisson实现分布式锁。
3.3 基于zookeeper
zookeeper节点的介绍:
- 持久节点
- 持久有序节点
- 临时节点
- 临时有序节点
基于zookeeper的分布式锁正是利用了临时有序节点的特征实现的。过程大概如下:
- 创建一个持久化节点ZKLock,
- Client1获取锁在ZKLock节点下创建一个临时有序节点Lock01,然后去ZKLock节点下比较所有的节点,判断自己是不是最小的,如果是最小的则获取锁成功。
- Client2这时再去获取锁时在ZKLock节点下创建一个临时有序节点Lock02,然后判断Lock02是不是最小节点,发现自己不是最小节点,则向它的前一个节点也就是client1注册Watcher事件,这就意味着Client2获取锁失败,进入等待状态
- Client3进来时,先创建一个Lock03节点,判断Lock03是不是当前最小节点,显然Lock03并不是最小的锁,则向Client2注册Watcher事件,并进入等待状态
- client1执行完成后,主动释放锁
- client1在主动释放锁之前宕机了,则根据临时节点的特性,zk会自动删除临时节点Lock1
4、Redisson
摘自官网一段话:
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet
, Set
, Multimap
, SortedSet
, Map
, List
, Queue
, BlockingQueue
, Deque
, BlockingDeque
, Semaphore
, Lock
, AtomicLong
, CountDownLatch
, Publish / Subscribe
, Bloom filter
, Remote service
, Spring cache
, Executor service
, Live Object service
, Scheduler service
) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
这里我们只简单介绍一下分布式锁,它为我们提供了可重入锁(ReentrantLock),公平锁(Fair Lock),读写锁(ReadWriteLock),信号量(Semaphore)等等。
简单使用方法,这里以可重入锁为例,其他可以参考官网
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
/ 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
同时还提供了支持异步的方法:
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
Redisson官网: https://github.com/redisson/redisson
中文文档: https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
5、代码分析
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
public class InitRedisClockController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private Redisson redisson;
@GetMapping("/initRedis")
public String initRedisClock(){
redisTemplate.opsForValue().set("redis_lock",100);
return "OK";
}
@GetMapping("/deductLock")
public String deductLock(){
//添加synchronized,使用jmeter压测后发现会有超买的现象,这种在单台的服务器下没有问题,在分布式下有问题
synchronized (this){
int value = Integer.parseInt(redisTemplate.opsForValue().get("redis_lock").toString());
if(value>0){
int leastValue = value -1;
redisTemplate.opsForValue().set("redis_lock",leastValue);
System.out.println("秒杀成功,剩余库存:"+leastValue);
}else{
System.out.println("秒杀失败,库存不足");
}
return "end";
}
}
@GetMapping("/deductLock2")
public String deductLock2(){
//使用redis的setnx命令,添加一个分布式锁,并设置一个锁的失效时间,防止死锁的问题,超过时间后redis自动释放该锁
//这个写有一个问题,就是在高并发下,A线程加的锁会被B线程给释放,
//此代码问题:A线程获得锁后 执行程序需要15秒,在10秒后 redis会自动释放锁
// 此时 B线程就会获得锁,然后继续执行,这个A线程完成了任务,则回去释放锁,此时C线程就会进行,
//在高并发下这种问题是致命的,会导致所有的程序错乱
//这里可以把锁的超时时间设置长一点,但是,时间多长合适呢?
try {
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("fbs_lock", "wangxin",10, TimeUnit.SECONDS);
if(!aBoolean){
System.out.println("没有获取到锁");
return "error";
}
int value = Integer.parseInt(redisTemplate.opsForValue().get("redis_lock").toString());
if(value>0){
int leastValue = value -1;
redisTemplate.opsForValue().set("redis_lock",leastValue);
System.out.println("秒杀成功,剩余库存:"+leastValue);
}else{
System.out.println("秒杀失败,库存不足");
}
return "end";
} finally {
redisTemplate.delete("fbs_lock");
}
}
@GetMapping("/deductLock3")
public String deductLock3(){
//添加一个uuid作为唯一标识,判断锁只能由自己释放,不能被其他线程释放
//此代码问题:A线程获得锁后 执行程序需要15秒,在10秒后 redis会自动释放锁
// 此时 B线程就会获得锁,但是A线程还没有执行完毕,所以这样还是会在并发执行。
String uuid = UUID.randomUUID().toString();
try {
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("fbs_lock", uuid,10, TimeUnit.SECONDS);
if(!aBoolean){
System.out.println("没有获取到锁");
return "error";
}
int value = Integer.parseInt(redisTemplate.opsForValue().get("redis_lock").toString());
if(value>0){
int leastValue = value -1;
redisTemplate.opsForValue().set("redis_lock",leastValue);
System.out.println("秒杀成功,剩余库存:"+leastValue);
}else{
System.out.println("秒杀失败,库存不足");
}
return "end";
} finally {
//判断自己的锁只能自己去释放,其他线程不能释放
if(uuid.equals(redisTemplate.opsForValue().get("fbs_lock"))){
redisTemplate.delete("fbs_lock");
}
}
}
@GetMapping("/deductLock4")
public String deductLock4(){
//针对上面的问题,使用Redisson解决
String uuid = UUID.randomUUID().toString();
RLock lock = redisson.getLock("fbs_lock");
try {
lock.lock();
int value = Integer.parseInt(redisTemplate.opsForValue().get("redis_lock").toString());
if(value>0){
int leastValue = value -1;
redisTemplate.opsForValue().set("redis_lock",leastValue);
System.out.println("秒杀成功,剩余库存:"+leastValue);
}else{
System.out.println("秒杀失败,库存不足");
}
return "end";
} finally {
lock.unlock();
}
}
}