需求分析
所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。
秒杀商品通常有两种限制:库存限制、时间限制。
(1)商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。
(2)秒杀下单成功,直接跳转到支付页面(支付宝扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
(3)当用户秒杀下单5分钟内未支付,取消预订单,调用支付宝支付的关闭订单接口,恢复库存。
秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力!读取商品详细信息时运用缓存,当用户点击抢购时减少缓存中的库存数量,当库存数为0时或活动期结束时,同步到数据库。产生的秒杀预订单也不会立刻写到数据库中,而是先写到缓存,当用户付款成功后再写入数据库。
实现
秒杀商品的查询
- 判断redis是否为空,如果不为空,就从redis中查询
- 如果为空就从数据库中查询并将查询结果存储到redis中
public List<TbSeckillGoods> findList() {
List<TbSeckillGoods> secKillGoods = redisTemplate.boundHashOps("secKillGoods").values();
if (secKillGoods==null || secKillGoods.size()==0){
TbSeckillGoodsExample example = new TbSeckillGoodsExample();
TbSeckillGoodsExample.Criteria criteria = example.createCriteria();
criteria.andStatusEqualTo("1");
//当前时间大于开始时间,小于结束时间
criteria.andStartTimeLessThan(new Date());
criteria.andEndTimeGreaterThan(new Date());
//库存限制
criteria.andStockCountGreaterThan(0);
secKillGoods = seckillMapper.selectByExample(example);
//存入redis中
for (TbSeckillGoods secKillGood : secKillGoods) {
redisTemplate.boundHashOps("secKillGoods").put(secKillGood.getId(),secKillGood);
}
}
return secKillGoods;
}
秒杀订单的创建
- 订单在redis中存储的大键名任意,每个小键为用户id,值为订单的list集合
- 同一个商品一个用户只能秒杀一件
- 判断秒杀商品已经从秒杀完从redis中删除和秒杀商品数量小于等于0的情况(在高并发情况下,库存数量是有可能为负的)
- 扣减库存并更新到redis中
- 当库存扣减为0的时候,从缓存中移除 该秒杀商品, 同时将数据同步到 mysql中
- 将秒杀订单保存在redis中
public Long submitOrder(Long seckillId, String userId) {
//从redis中查询出用户的秒杀订单
List<TbSeckillOrder> OrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(userId);
if (OrderList==null){
OrderList = new ArrayList<>();
}
else {
for (TbSeckillOrder secKillorder : OrderList) {
if (secKillorder.getSeckillId().longValue() == seckillId.longValue()){//一个商品一个用户只能秒杀一次
throw new RuntimeException("你已经成功秒杀到了该商品!");
}
}
}
//从redis中获取当前秒杀商品的数据,判断该商品是否还能购买
TbSeckillGoods seckillGood = (TbSeckillGoods) redisTemplate.boundHashOps("secKillGoods").get(seckillId);
//判断秒杀商品已经从秒杀完从redis中删除和秒杀商品数量小于等于0的情况(在高并发情况下,库存数量是有可能为负的)
if (seckillGood==null || seckillGood.getStockCount()<=0){
throw new RuntimeException("很遗憾,该商品已经抢完,谢谢参与");
}
//扣减库存
seckillGood.setStockCount(seckillGood.getStockCount()-1);
redisTemplate.boundHashOps("secKillGoods").put(seckillId,seckillGood);
// 当库存扣减为0的时候,从缓存中移除 该秒杀商品, 同时将数据同步到 mysql中
if (seckillGood.getStockCount()==0){
redisTemplate.boundHashOps("secKillGoods").delete(seckillId);
seckillGoodsMapper.updateByPrimaryKey(seckillGood);
}
//将秒杀订单保存在redis中
TbSeckillOrder seckillOrder = new TbSeckillOrder();
long orderId = idWorker.nextId();
seckillOrder.setId(orderId);
seckillOrder.setSeckillId(seckillId);
seckillOrder.setMoney(seckillGood.getCostPrice());
seckillOrder.setUserId(userId);
seckillOrder.setSellerId(seckillGood.getSellerId());
seckillOrder.setCreateTime(new Date());
// 未付款
seckillOrder.setStatus("0");
//将订单加入用户的秒杀订单表
OrderList.add(seckillOrder);
redisTemplate.boundHashOps("secKillOrders").put(userId,OrderList);
//返回订单id
return orderId;
}
高并发压力测试
测试之前秒杀商品库存数量是10
在spring-security.xml文件中对测试url进行放行
使用jmeter进行测试,并发测试500
并发测试之后,库存清为0
在redis中查看创建订单的数量为17,发生了超卖现象
redis分布式锁解决超卖
我们通过单节点Redis实现一个分布式锁。
利用redis在同一时刻操作一个键的值只能有一个进程的特性,如果能设值成功就获取到锁;解锁,就是删除指定的键;
为防止死锁可以设置锁超时时间,如果锁超时就释放锁。
秒杀订单支付及保存
支付只需要我们从redis中将订单号和支付金额查询出,然后调用支付方法即可
页面监听订单是否支付
如果已支付,将订单保存到mysql数据库以及更新redis数据库的信息
将方法写在业务层还有个好处是,业务层切入了事务,如果没有执行成功,可以进行回滚
在支付控制层的支付成功判断里调用更新方法
public void updateSecKillOrder(String userId, String out_trade_no) {
TbSeckillOrder saveOrder = null;
List<TbSeckillOrder> secOrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(userId);
for (TbSeckillOrder seckillOrder : secOrderList) {
if (out_trade_no.equals(seckillOrder.getId().longValue()+"")){
seckillOrder.setPayTime(new Date());
seckillOrder.setStatus("1");
//同步到mysql
saveOrder = seckillOrder;
}
}
redisTemplate.boundHashOps("secKillOrders").put(userId,secOrderList);
seckillOrderMapper.insert(saveOrder);
}
秒杀付款超时
public void backAndRemoveOrder(String name, String out_trade_no) {
//还原库存
//先查询出超时未付款订单
TbSeckillOrder order = findSecKillOrderByUserIdAndOrderId(name, Long.valueOf(out_trade_no));
TbSeckillGoods seckillGood = (TbSeckillGoods) redisTemplate.boundHashOps("secKillGoods").get(order.getSeckillId());
if (seckillGood==null){
TbSeckillGoods dbGoods = seckillGoodsMapper.selectByPrimaryKey(order.getSeckillId());
// dbGoods可不可用就不一定了:可能在用户3分钟未付款期间,该商品到了秒杀截止时间,
// 此时就不用再添加到 redis缓存了, 但是 数据库 的库存 应该 +1
//过期
if (dbGoods.getEndTime().getTime()<new Date().getTime()){
dbGoods.setStockCount(dbGoods.getStockCount()+1);
seckillGoodsMapper.updateByPrimaryKey(dbGoods);
}else {
dbGoods.setStockCount(dbGoods.getStockCount()+1);
seckillGoodsMapper.updateByPrimaryKey(dbGoods);
//更新redis中的数据
redisTemplate.boundHashOps("secKillGoods").put(dbGoods.getId(),dbGoods);
}
}else {
seckillGood.setStockCount(seckillGood.getStockCount()+1);
//更新redis中的数据
redisTemplate.boundHashOps("secKillGoods").put(seckillGood.getId(),seckillGood);
}
//移除未付款订单
List<TbSeckillOrder> newList = new ArrayList<>();
List<TbSeckillOrder> secOrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(name);
for (TbSeckillOrder seckillOrder : secOrderList) {
if (out_trade_no.equals(seckillOrder.getId().longValue()+"")){
}else{
newList.add(seckillOrder);
}
}
redisTemplate.boundHashOps("secKillOrders").put(name,newList);
}
注意
list删除复杂对象使用remove没有效果,基本类型的数据可以删除