一、理论基础
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<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity);
@Async
private void createSeckillToken(Long seckillId, Long tokenQuantity) {
generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
}
2.2、秒杀商品服务接口
(1)SpikeCommodityService:
点击立即抢购(秒杀)按钮,接口传入电话或者用户ID+库存商品ID (秒杀 库存表的库存ID)
/**
* 秒杀商品服务接口
*/
public interface SpikeCommodityService {
/**
* 用户秒杀接口 phone和userid都可以的
*
* @phone 手机号码<br>
* @seckillId 库存id
* @return
*/
@RequestMapping("/spike")
public BaseResponse<JSONObject> spike(String phone, Long seckillId);
/**
* 新增对应商品库存令牌桶
*
* @seckillId 商品库存id
*/
@RequestMapping("/addSpikeToken")
public BaseResponse<JSONObject> 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<JSONObject> implements SpikeCommodityService {
@Autowired
private SeckillMapper seckillMapper;
@Autowired
private GenerateToken generateToken;
@Autowired
private SpikeCommodityProducer spikeCommodityProducer;
@Override
@Transactional
@HystrixCommand(fallbackMethod = "spikeFallback")
public BaseResponse<JSONObject> spike(String phone, Long seckillId) {
// 1.参数验证
if (StringUtils.isEmpty(phone)) {
return setResultError("手机号码不能为空!");
}
if (seckillId == null) {
return setResultError("商品库存id不能为空!");
}
// 2.从redis从获取对应的秒杀token
String seckillToken = generateToken.getListKeyToken(seckillId + "");
if (StringUtils.isEmpty(seckillToken)) {
log.info(">>>seckillId:{}, 亲,该秒杀已经售空,请下次再来!", seckillId);
return setResultError("亲,该秒杀已经售空,请下次再来!");
}
// 3.获取到秒杀token之后,异步放入mq中实现修改商品的库存
sendSeckillMsg(seckillId, phone);
return setResultSuccess("正在排队中.......");
}
/**
* 获取到秒杀token之后,异步放入mq中实现修改商品的库存
*/
@Async
private 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
@Override
public BaseResponse<JSONObject> 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("令牌正在生成中.....");
}
@Async
private 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实现修改库存
若对你有帮助,欢迎关注!!点赞!!评论!!