实战高并发秒杀实现(2):防止库存超卖问题(超详细)

一、理论基础

1.1、防止库存超卖,需要关注的问题

(1)秒杀数据库的设计;

(2)基于数据库乐观锁防止库存超卖;

(3)基于redis实现用户行为频率限制——用户再次抢购时提示“该用户操作频繁,请少稍后重试,一般可设置10秒后才能再次调用秒杀接口”;

(4)基于Token令牌+MQ实现异步修改库存;

(5)使用apache-jmeter做秒杀压力测试(可配置线程数和循环次数(每个线程跑多少个请求数),比如线程数100循环次数100,则模式10000次请求)。

1.2、数据库崩溃问题

问题:

如果秒杀的请求过多,对数据库频繁的IO操作,可能会产生数据库崩溃问题。这时搞分表分库、读写分离、做缓存、限流、熔断都不会起作用。——最有用的是,提前生成令牌,存放在临牌桶中,异步发送到MQ中(token只经过缓存不经过数据库),MQ异步修改库存

解决方案:

假设库存有100个,但是可能会有10万个并发,要解决数据库频繁IO,可以提前生成好数据库 库存个数个Token,比如这里是100个Token,比如这时有10万个并发,谁能抢到Token,再把Token扔到MQ里面,在MQ里面异步实现修改库存。这时就能做到多少个库存有多少个请求到数据库,而不是10万个请求访问10万次数据库,防止了没抢到的用户无法修改数据库,从而减少了IO操作

1.3、秒杀骨架图

1.4、总体实现步骤

(1)后台系统在发布秒杀商品的时候,给对应商品添加库存token;

二、实战

2.1、秒杀数据库设计

(1)秒杀 成功明细(记录)表

秒杀抢购的订单和普通下单的订单是完全不一样的

CREATE TABLE `shop_order` (`seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品id',`user_phone` bigint(20) NOT NULL COMMENT '用户手机号',`state` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '状态标示:-1:无效 0:成功 1:已付款 2:已发货',`create_time` datetime NOT NULL COMMENT '创建时间',KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

(2)秒杀 库存表

比如jmeter模拟时发出10000个请求,但是库存只有100,则有9900个用户抢不到。

version字段:代表更新次数,默认是0,库存没更新一次则加1,比如库存1000,变为997,则version值变为3,并且成功明细表有3条数据。

CREATE TABLE `shop_seckill` (`seckill_id` bigint(20) NOT NULL COMMENT '商品库存id',`name` varchar(120) CHARACTER SET utf8 NOT NULL COMMENT '商品名称',`inventory` int(11) NOT NULL COMMENT '库存数量',`start_time` datetime NOT NULL COMMENT '秒杀开启时间',`end_time` datetime NOT NULL COMMENT '秒杀结束时间',`create_time` datetime NOT NULL COMMENT '创建时间',`version` bigint(20) NOT NULL DEFAULT '0',PRIMARY KEY (`seckill_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀库存表';

2.2、后台系统在发布秒杀商品的时候,给对应商品添加库存token

新增对应商品令牌桶:

/*** 新增对应商品库存令牌桶* @seckillId 商品库存id*/@RequestMapping("/addSpikeToken")public BaseResponse addSpikeToken(Long seckillId, Long tokenQuantity);@Asyncprivate void createSeckillToken(Long seckillId, Long tokenQuantity) {generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);}

2.2、秒杀商品服务接口

(1)SpikeCommodityService:

点击立即抢购(秒杀)按钮,接口传入电话或者用户ID+库存商品ID (秒杀 库存表的库存ID

/*** 秒杀商品服务接口*/
public interface SpikeCommodityService {/*** 用户秒杀接口 phone和userid都可以的* * @phone 手机号码
* @seckillId 库存id* @return*/@RequestMapping("/spike")public BaseResponse spike(String phone, Long seckillId);/*** 新增对应商品库存令牌桶* * @seckillId 商品库存id*/@RequestMapping("/addSpikeToken")public BaseResponse addSpikeToken(Long seckillId, Long tokenQuantity);}

(2)SpikeCommodityServiceImpl:

秒杀接口实现步骤:

  • 参数验证;
  • 从redis从获取对应的秒杀token——采用redis数据库类型为 list类型, key为 商品库存id ,list存 多个秒杀token,一个库存ID可以装多个秒杀token;
  •  获取到秒杀token之后,异步(@Async异步注解)放入mq中实现修改商品的库存——多线程异步生成token
  • 方法添加@HystrixCommand(fallbackMethod = "spikeFallback")——实现服务隔离和降级
@RestController
@Slf4j
public class SpikeCommodityServiceImpl extends BaseApiService implements SpikeCommodityService {@Autowiredprivate SeckillMapper seckillMapper;@Autowiredprivate GenerateToken generateToken;@Autowiredprivate SpikeCommodityProducer spikeCommodityProducer;@Override@Transactional@HystrixCommand(fallbackMethod = "spikeFallback")public BaseResponse spike(String phone, Long seckillId) {// 1.参数验证if (StringUtils.isEmpty(phone)) {return setResultError("手机号码不能为空!");}if (seckillId == null) {return setResultError("商品库存id不能为空!");}// 2.从redis从获取对应的秒杀tokenString seckillToken = generateToken.getListKeyToken(seckillId + "");if (StringUtils.isEmpty(seckillToken)) {log.info(">>>seckillId:{}, 亲,该秒杀已经售空,请下次再来!", seckillId);return setResultError("亲,该秒杀已经售空,请下次再来!");}// 3.获取到秒杀token之后,异步放入mq中实现修改商品的库存sendSeckillMsg(seckillId, phone);return setResultSuccess("正在排队中.......");}/*** 获取到秒杀token之后,异步放入mq中实现修改商品的库存*/@Asyncprivate void sendSeckillMsg(Long seckillId, String phone) {JSONObject jsonObject = new JSONObject();jsonObject.put("seckillId", seckillId);jsonObject.put("phone", phone);spikeCommodityProducer.send(jsonObject);}/*** 使用多线程异步生产令牌* * @param seckillId* @param tokenQuantity* @return*/// 采用redis数据库类型为 list类型 key为 商品库存id list 多个秒杀token@Overridepublic BaseResponse addSpikeToken(Long seckillId, Long tokenQuantity) {// 1.验证参数if (seckillId == null) {return setResultError("商品库存id不能为空!");}if (tokenQuantity == null) {return setResultError("token数量不能为空!");}SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);if (seckillEntity == null) {return setResultError("商品信息不存在!");}// 2.使用多线程异步生产令牌createSeckillToken(seckillId, tokenQuantity);return setResultSuccess("令牌正在生成中.....");}@Asyncprivate void createSeckillToken(Long seckillId, Long tokenQuantity) {generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);}}

(3)修改库存:SeckillMapper

方案1:行锁机制(悲观锁)——数据库自带

如果不适用乐观锁防止超卖,直接更新数据库时使用一个“and inventory>0方式”——库存大于零才更新就可以了。

然后,mysql中每次在更新数据库时有行锁机制(悲观锁),不存在超卖问题

update shop_seckill set inventory=inventory-1,where  seckill_id=#{seckillId} and inventory>0 

方案2:version乐观锁实现乐观锁是通过version版本号控制的

version版本号乐观锁机制:

多个线程同时update的时候只有一个能成功,谁成功谁拿到锁。成功的线程修改成功时版本号加1,所以其余的线程就无法更新了(因为版本号变了)

update shop_seckill set inventory=inventory-1, version=version+1 where  seckill_id=#{seckillId} and inventory>0  and version=#{version} ;

以下是悲观锁的实现: 

public interface SeckillMapper {/*** 使用乐观锁修改库存信息 and inventory>0方式* @param seckillId* @return*/@Update("update meite_seckill set inventory=inventory-1 where  seckill_id='10001' and inventory>0")int optimisticLockSeckill(Long seckillId);/*** 基于版本号形式实现乐观锁* @param seckillId* @return*/@Update("update meite_seckill set inventory=inventory-1 ,version=version+1 where  seckill_id=#{seckillId} and version=#{version} and inventory>0;")int optimisticVersionSeckill(@Param("seckillId") Long seckillId, @Param("version") Long version);@Update("update meite_seckill set inventory=inventory-1 where  seckill_id='10001';")int inventoryDeduction(Long seckillId);@Select("SELECT seckill_id AS seckillId,name as name,inventory as inventory,start_time as startTime,end_time as endTime,create_time as createTime,version as version from meite_seckill where seckill_id=#{seckillId}")SeckillEntity findBySeckillId(Long seckillId);}

  上一篇:抢购理论研究

  下一篇:基于token+MQ实现修改库存

若对你有帮助,欢迎关注!!点赞!!评论!!


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部