淘东电商项目(74) -秒杀系统(库存超卖解决方案)

简介: 淘东电商项目(74) -秒杀系统(库存超卖解决方案)

引言

本文代码已提交至Github(版本号:4a1e952df7a06cb764166262b02c8c23962e6084),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop

在上一篇博客《淘东电商项目(73) -秒杀系统(前端优化)》主要讲解了秒杀系统的前端优化,本文开始讲解后端的秒杀系统设计。

本文目录结构:

l____引言

l____ 1.什么是库存超卖?

l____ 2.库存超卖的解决方案

l________ 2.1 解决方案

l________ 2.2 数据库表设计

l________ 2.3 使用DB行锁(悲观锁)

l________ 2.4 使用version控制(乐观锁)

l____ 3. 测试

l________ 3.1 测试悲观锁

l________ 3.2 测试乐观锁

1.什么是库存超卖?

在秒杀系统中,同一时刻大量的用户会并发访问秒杀接口,此时数据库会相应的减少库存,举个例子:

比如一件商品有100件,此时有10万个用户同时访问秒杀接口,当数据库还剩一件商品时,A用户和B用户同时进入接口,操作数据库,都做扣减库存操作(set sum=sum-1),由于数据库的行锁机制,A用户先获取到行锁,所以A用户获取后,库存应该为0(即当前库存-1)。A用户操作完后,释放行锁,B用户进行操作,库存变为-1(即当前库存-1),这很明显是不符合需求的,那该如何解决呢?下面来讲解。

2.库存超卖的解决方案

2.1 解决方案

为了应对库存超卖的问题,有两种解决方案:

  • 使用DB行锁,也就是悲观锁(WHERE控制)。
  • 使用version控制,也就是乐观锁(CAS无锁机制)。

2.2 数据库表设计

讲解代码前,先看看秒杀系统数据库的表设计:

①订单表:

CREATE TABLE `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='秒杀成功明细表';

②秒杀库存表:

CREATE TABLE `seckill` (
  `seckill_id` bigint(20) NOT NULL COMMENT '商品库存id',
  `name` varchar(120) 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' COMMENT '乐观锁',
  PRIMARY KEY (`seckill_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';

2.3 使用DB行锁(悲观锁)

首先看看秒杀接口的代码逻辑:

@Transactional
public BaseResponse<JSONObject> spike(String phone, Long seckillId) {
  // TODO 1.参数验证
  // TODO 2.用户频率限制 setnx 如果key存在话
  // TODO 3.修改数据库对应的库存 1万中只有100个抢购成功 提前生成好100个token 谁能够抢购成功toen放入到mq中实现异步修改库存
  // TODO 4.添加秒杀成功订单 基于MQ实现异步形式
}

库存超卖逻辑在第3个步骤,下面直接贴出Mybatis SQL语句:

update
  seckill 
set 
  inventory=inventory-1 
where  
  seckill_id=#{seckillId} and inventory>0;

上面的语句主要是由where来控制,在inventory(库存数量)大于0的情况下,才允许修改库存减一。

缺点:由于DB里面使用的是行锁,所以效率比较低,要等一个更新操作完才能进行下一个更新操作,在用户并发量高的情况下,效率非常慢

解决方案:使用version控制,即乐观锁,下面讲解。

2.4 使用version控制(乐观锁)

注意:乐观锁CAS无锁机制主要的两个变量:“预期值"和"结果值”

下面看看使用乐观锁之后的MyBatis SQL语句:

①首先获取当前乐观锁的version版本号:

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 
  seckill 
where 
  seckill_id=#{seckillId}

②然后传入查询的乐观锁的version版本号,并更新库存:

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

优点:效率高同时也防止库存超卖

3. 测试

首先数据库模拟插入一条数据:

INSERT INTO `seckill`(`seckill_id`, `name`, `inventory`, `start_time`, `end_time`, `create_time`, `version`) VALUES (100001, 'iphoneX', 100, '2020-05-25 17:16:11', '2020-05-25 17:16:13', '2020-05-25 17:16:16', 1);

使用JMeter测试,定义200个用户访问:

3.1 测试悲观锁

调用悲观锁接口pessimisticDeduction,核心代码如下:

@Transactional
public BaseResponse<JSONObject> spike(String phone, Long seckillId) {
  // 1.参数验证
  if (StringUtils.isEmpty(phone)) {
    return setResultError("手机号码不能为空!");
  }
  if (seckillId == null) {
    return setResultError("商品库存id不能为空!");
  }
  SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
  if (seckillEntity == null) {
    return setResultError("商品信息不存在!");
  }
  // 2.用户频率限制 setnx 如果key存在话
  // 3.(悲观锁 )修改数据库对应的库存 1万中只有100个抢购成功 提前生成好100个token 谁能够抢购成功token放入到mq中实现异步修改库存
  int inventoryDeduction = seckillMapper.pessimisticDeduction(seckillId);
  if (!toDaoResult(inventoryDeduction)) {
    log.info(">>>修改库存失败>>>>inventoryDeduction返回为{} 秒杀失败!", inventoryDeduction);
    return setResultError("亲,请稍后重试!");
  }
  // 4.添加秒杀成功订单 基于MQ实现异步形式
  OrderEntity orderEntity = new OrderEntity();
  orderEntity.setUserPhone(phone);
  orderEntity.setSeckillId(seckillId);
  int insertOrder = orderMapper.insertOrder(orderEntity);
  if (!toDaoResult(insertOrder)) {
    return setResultError("亲,请稍后重试!");
  }
  log.info(">>>修改库存成功>>>>inventoryDeduction返回为{} 秒杀成功", inventoryDeduction);
  return setResultSuccess("恭喜您,秒杀成功!");
}

运行JMeter,可以看到数据库的库存减为0,并新增了100条订单:

运行前 运行后

3.2 测试乐观锁

调用乐观锁接口optimisticDeduction,核心代码如下:

@Transactional
public BaseResponse<JSONObject> spike(String phone, Long seckillId) {
  // 1.参数验证
  if (StringUtils.isEmpty(phone)) {
    return setResultError("手机号码不能为空!");
  }
  if (seckillId == null) {
    return setResultError("商品库存id不能为空!");
  }
  SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
  if (seckillEntity == null) {
    return setResultError("商品信息不存在!");
  }
  // 2.用户频率限制 setnx 如果key存在话
  // 3.(乐观锁 )修改数据库对应的库存 1万中只有100个抢购成功 提前生成好100个token 谁能够抢购成功token放入到mq中实现异步修改库存
  Long version = seckillEntity.getVersion();
  int inventoryDeduction = seckillMapper.optimisticDeduction(seckillId, version);
  if (!toDaoResult(inventoryDeduction)) {
    log.info(">>>修改库存失败>>>>inventoryDeduction返回为{} 秒杀失败!", inventoryDeduction);
    return setResultError("亲,请稍后重试!");
  }
  // 4.添加秒杀成功订单 基于MQ实现异步形式
  OrderEntity orderEntity = new OrderEntity();
  orderEntity.setUserPhone(phone);
  orderEntity.setSeckillId(seckillId);
  int insertOrder = orderMapper.insertOrder(orderEntity);
  if (!toDaoResult(insertOrder)) {
    return setResultError("亲,请稍后重试!");
  }
  log.info(">>>修改库存成功>>>>inventoryDeduction返回为{} 秒杀成功", inventoryDeduction);
  return setResultSuccess("恭喜您,秒杀成功!");
}

运行JMeter,可以看到数据库的库存减少了24个,并新增了24条订单:

运行前 运行后
相关实践学习
快速体验阿里云云消息队列RocketMQ版
本实验将带您快速体验使用云消息队列RocketMQ版Serverless系列实例进行获取接入点、创建Topic、创建订阅组、收发消息、查看消息轨迹和仪表盘。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
目录
相关文章
|
SQL Java 数据库
Spring Boot 的事务控制及示例代码
Spring Boot 提供了简单易用的事务控制功能,方便开发者进行数据库操作时保证数据的一致性和完整性。本文将介绍 Spring Boot 事务控制的用法和应用场景,并提供丰富的例子。
540 2
|
3月前
|
关系型数据库 MySQL Java
MySQL 分库分表 + 平滑扩容方案 (秒懂+史上最全)
MySQL 分库分表 + 平滑扩容方案 (秒懂+史上最全)
|
缓存 NoSQL 安全
|
12月前
|
缓存 NoSQL 应用服务中间件
【开发系列】秒杀系统的设计
【开发系列】秒杀系统的设计
|
11月前
|
网络协议 算法 数据库
OSPF中的Router LSA详解
OSPF中的Router LSA详解
432 4
|
12月前
|
存储 缓存 固态存储
固态硬盘寿命一般多少年?
随着科技的飞速发展,固态硬盘(SSD)已经成为现代计算机存储设备的重要组成部分。相比传统的机械硬盘(HDD),固态硬盘具有速度更快、抗震性强、功耗低、噪音小等优点。不过,很多人对固态硬盘的寿命依然存在疑虑。本期内容就要和大家深度聊一聊固态硬盘寿命的方方面面。
固态硬盘寿命一般多少年?
|
JavaScript 网络安全 iOS开发
如何用 Electron 打包chatgpt-plus.top并生成mac客户端
如何用 Electron 打包chatgpt-plus.top并生成mac客户端
240 0
|
编解码 前端开发 开发者
构建响应式网页布局的终极指南
【2月更文挑战第18天】 随着移动互联网的兴起,响应式网页设计成为前端开发者必须掌握的核心技能之一。本文旨在提供一个全面的指南来帮助开发者理解并实现灵活且高效的响应式布局。我们将深入探讨媒体查询、弹性盒模型、相对单位等关键技术,并通过实例演示如何结合这些技术创建适应不同屏幕尺寸的网页。文章的目标是让读者能够独立设计和开发出在各种设备上均能提供优秀用户体验的响应式网站。
316 1
|
SQL NoSQL 关系型数据库
【并发】高并发下库存超卖问题如何解决?
【并发】高并发下库存超卖问题如何解决?
5791 0
|
JavaScript Java 关系型数据库
实例!使用Idea创建SSM框架的Maven项目
实例!使用Idea创建SSM框架的Maven项目