参考B站视频资源:BV1Vt4y1D7BH,程序员诸葛
一、基本介绍
1.1 超卖场景
不同用户在读请求的时候,发现商品库存足够,然后同时发起请求,进行秒杀操作,减库存,导致库存减为负数。
——主要解决方案:Redis分布式锁、MQ队列
1.2 普通加锁
如果直接对减库存代码加同步锁,由于分布式系统有多个Tomcat,前端请求会被分发到不同的Tomcat上去,Tomcat会各自访问数据库,这样加锁就控制不了。
1.3 解决方案——Redis的分布式锁(Jedis和Redisson)
——用分布式锁,用Redis中的setnx命令:setnx key value
可实现分布式锁。
setnx跟set的区别:
① set key v1,set key,v2,v2会覆盖v1;
② 用setnx,将key设置为value时,当且仅当key不存在,若存在,不会做任何动作。
setnx被封装到了Jedis和Redisson中,Jedis和Redisson是Java中对Redis操作的封装框架:
- (1)Jedis 只是简单的封装了 Redis 的API库,可以看作是Redis客户端,它的方法和Redis 的命令很类似;
- (2)Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于Jedis 更强大。
命令分别为:stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, timeout:30, TimeUnit.SECONDS);
和redisson.lock();
多个Tomcat都去到Redis中排队(因为Redis是单线程的,不管多少分布式的Tomcat请求过来,都会到Redis中去排队)。
二、高并发分布式锁——Jedis
以下代码相当于:jedis.setnx(key,value)
,
加锁:设置一个key:lockKey
@AutoWire
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/stock");//注解SpringMVC
public String deductStock(){
String lockKey = "lockKey";
String clienId = UUID.randomUUID().toString();//加一个本线程的客户ID
try{
//锁加30s过期时间,这样高并发下锁会提前失效,导致超卖,需加ID,在后面判断线程结束才释放锁
//既然线程结束就会释放锁,为和还要超时时间?——担心后面程序挂掉,而导致死锁
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, timeout:30, TimeUnit.SECONDS); //相当于jedis.setnx(key,value)
if(!result){
return “error_code”;//前面线程设置了key,后面就没法设置,失败就返回error_code
}
//减库存操作
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock")
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("扣减成功,剩余库存" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}finally{
if(clienId.equals(stringRedisTemplate.opsForValue().get(lockKey)));//跟本线程ID对比,确保本线程执行完后才删除相应锁
stringRedisTemplate.delete(lockKey);//业务结束,释放锁
}
}
2.1 程序思路
- 上述设置了:在主线程即减库存线程结束就删除锁,以及设置锁超时时间。
- 既然线程结束就会释放锁,为何还要超时时间?——担心后面程序挂掉,而导致死锁
2.1.1 锁延期
但是也可能会出现线程未结束锁就超时了,锁超时时间又不能设置得太久,怎么办?
——可另外设置一个分线程来开启一个定时任务,每隔一段时间检查一下主线程在锁快超时时有没有执行完,若没有执行完,则把锁过期时间继续设置为初始值30s —— 锁延期
三、高并发分布式锁——Redisson
Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于Jedis 更强大,特别是在分布式领域
3.1 具体实施
(1)先引入Redisson依赖包3.6.5:
@SpringBoorApplication
public class Application{
public static void main(String[] args){
SpringApplication.run(Application.class,args);
}
@Bean
public Redisson redisson(){//redisson客户端,注入到Bean容器中
}
}
(2)在controller中注入:
@AutoWire
private Redisson redisson;
@AutoWire
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/stock");//注解SpringMVC
public String deductStock(){
String lockKey = "lockKey";
RLock redissonLock = redisson.getLock(lockKey);//(1)拿一个Redisson的锁对象
try{
redisson.lock();//(2)加锁,底层默认设置超时时间为30s
//(3)减库存操作
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock")
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("扣减成功,剩余库存" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}finally{
redissonLock.lock();//(4)释放锁
}
}
3.2 程序思路
程序到了(2)处,有且只有一个线程能继续往下执行,其它线程都会阻塞在(2)处,直到该线程被释放掉,其它线程才能加锁继续。**加锁的后台会每隔一段时间去检测线程持有的锁是否存在,还在的话,就延迟锁超时时间,即锁延期。
3.3 Redisson分布式锁底层原理
多个线程去执行lock操作,仅有一个线程能够加锁成功,其它线程循环阻塞。加锁成功,锁超时时间默认30s,并开启后台线程,加锁的后台会每隔10秒去检测线程持有的锁是否存在,还在的话,就延迟锁超时时间,重新设置为30s,即锁延期。
对于原子性,Redis分布式锁底层借助Lua脚本实现锁的原子性。
锁延期是通过在底层用Lua进行延时,延时检测时间是对超时时间timeout /3
- Redis一般可以设置多级缓存,第一台Redis服务器作为主服务器,第二台作为备用的。称作主从或者哨兵
- 或者搭建一个Redis集群架构
3.4 分布式锁经典面试题
问题:如果Redis是用了集群架构,当主Redis加锁了,开始执行线程,若还没来得及将锁通过异步同步的方式同步到从Redis节点,主节点就挂了,此时会把某一台从节点作为新的主节点,此时别的线程就可以加锁了,这样就出错了,怎么办?
3.4.1 解答一:使用zookeeper代替Redis
分布式锁也可以通过zookeeper来实现,它加锁成功后返回到Redisson线程客户端返回的时间,会有一定的延时(需要超过半数的节点加锁成功才会返回),通过牺牲可用性来保证一致性。
3.4.2 解答二:利用RedLock
——超过半数的Redis节点加锁成功才算成功
这样会造成性能问题,好像将高并发请求,用来串行执行了。
分布式锁的本质:是将并行请求,变成了串行请求。
——一致性zookeeper更好,并发性Redis更好,根据业务场景来选择。