概述:限时秒杀活动在我们的日常生活中有很多,尤其在“双11”,“618”这类购物节活动中用户的并发数更是海量剧增,那么系统为了防止“超卖”秒杀商品,怎么做才能不影响性能的同时防止超卖。
为了解决“超卖”问题有两种方案:
(1)乐观锁
(2)分布式锁
场景:现有10台IPhone14ProMax,在12:00正式发售;假设现有1000个用户同时抢购
表结构:
一、乐观锁:
模拟环境:存在单个秒杀服务,一个商品服务与一个订单服务;直接想秒杀服务发送请求做秒杀,秒杀服务业务层中调用商品服务的减库存业务方法与新增订单。
思想:基于版本号或时间戳的机制,假设并发操作之间不会产生冲突,只在提交时检查是否有冲突
实现步骤:
1、为shop_product添加一个字段version初始为0充当秒杀乐观锁
2、修改原有的SQL
原下订单修改商品库存的sql:update shop_product set stock=#{stock} where pid=#{pid}
在多用户同时秒杀商品时会出现“超卖”问题,可能两个线程同时读取的库存为1,然后都完成了秒杀操作订购了商品。引入字段version充当乐观锁:每次秒杀商品做库存修改时都会判断与当前version是否为你读的version值,一致说明你操作的过程中别的线程未修改,你就完成秒杀操作version++。 引入version后的sql语句:
使用Jmeter压力测试工具进行秒杀模拟测试:
postman测试服务间通信成功
二、分布式锁:
模拟环境:存在两个秒杀服务,一个商品服务与一个订单服务;为了简便在订单服务中使用feign的方式向秒杀服务发送请求,秒杀服务业务层中调用商品服务的减库存业务方法与新增订单。
思想:确保在分布式环境下只有一个进程或线程可以访问共享资源
Redis实现分布式锁:利用Redis的setnx命令,这个命令的特征是如果多次执行,只有第一次执行会成功,可以实现互斥的效果。保证获取锁失败后,不再一直获取,尝试一次,成功返回true,失败返回false。
获取锁:
●互斥:确保只有一个线程获取锁
●非阻塞设计,保证获取锁失败后,不再一直获取,尝试一次,成功返回true,失败返回false
○SETNX lock thread1
释放锁:
●手动释放
○DEL key 释放锁,删除即可
实现步骤:
1、引入redis坐标,装配StringRedisTemplate类
2、修改秒杀商品更新库存操作的业务层代码:
命令实现:(1)利用setnx命令存储数据的特点:不存在,返回true;存在,返回false
expire命令为key设置有效过期时间,防止服务宕机锁一直不背释放
(2)利用set命令添加nx和ex的选项:
● NX:与setnx一致,第一次执行成功
●EX:设置过期时间
set key value [EX time秒] [NX] 例如:set lock thread1 EX 10 NX
代码实现:调用setIfAbsent方法等同于set添加nx和ex的选项,保证setnx和expire命令的原子性
在秒杀服务的业务层调用商品服务的减库存与新增订单方法前对分布式锁的获取(setnx)进行判断,获取的线程可以执行秒杀操作,下单成功;同时操作完成后要释放分布式锁(即删除key)
Jmeter压力测试:
初始商品库存信息
用postman测试服务间通信没问题
商品shop_product表
秒杀订单表
总结:
- 乐观锁用于解决单体秒杀服务的“超卖”问题,能处理高并发,性能高。通过添加version字段,秒杀时会判断当前的version与库中的version是否一致,每次秒杀成功都会对version++的方式实现乐观锁。
- 分布式锁用于解决多个秒杀服务的“超卖”问题,通过获取分布式锁保证单个秒杀的节点同一时刻只能被一个线程所持有,秒杀操作完成后释放分布式锁。(Redis的setnx命令实现)