秒杀/高并发解决方案+落地实现
- 前面我们防止超卖 是通过到数据库查询和到数据库抢购,来完成的, 代码如下:
- 如果在短时间内,大量抢购冲击 DB, 造成洪峰, 容易压垮数据库
- 解决方案:使用 Redis 完成预减库存,如果没有库存了,直接返回,减小对 DB 的压力。
- 图示:
Redis 的预减,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。
修改 SeckillController.java 实现 InitializingBean
接口,
InitializingBean 当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容
package com.rainbowsea.seckill.controller;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rainbowsea.seckill.pojo.Order;
import com.rainbowsea.seckill.pojo.SeckillOrder;
import com.rainbowsea.seckill.pojo.User;
import com.rainbowsea.seckill.service.GoodsService;
import com.rainbowsea.seckill.service.OrderService;
import com.rainbowsea.seckill.service.SeckillOrderService;
import com.rainbowsea.seckill.vo.GoodsVo;
import com.rainbowsea.seckill.vo.RespBeanEnum;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;import javax.annotation.Resource;
import java.util.List;@Controller
@RequestMapping("/seckill")
// InitializingBean 当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容
public class SeckillController implements InitializingBean {// 装配需要的组件/对象@Resourceprivate GoodsService goodsService;@Resourceprivate SeckillOrderService seckillOrderService;@Resourceprivate OrderService orderService;@Resourceprivate RedisTemplate redisTemplate;/*** 方法: 处理用户抢购请求/秒杀* 说明: 我们先完成一个 V3.0版本,* - 利用 MySQL默认的事务隔离级别【REPEATABLE-READ】* - 使用 优化秒杀: Redis 预减库存+Decrement** @param model 返回给模块的 model 信息* @param user User 通过用户使用了,自定义参数解析器获取 User 对象,* @param goodsId 秒杀商品的 ID 信息* @return 返回到映射在 resources 下的 templates 下的页面*/@RequestMapping(value = "/doSeckill")public String doSeckill(Model model, User user, Long goodsId) {System.out.println("秒杀 V 2.0 ");if (null == user) { //用户没有登录return "login";}// 登录了,则返回用户信息给下一个模板内容model.addAttribute("user", user);// 获取到 GoodsVoGoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);// 判断库存if (goodsVo.getStockCount() < 1) { // 没有库存,不可以购买model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());return "secKillFail"; // 返回一个错误页面}// 判断用户是否复购-直接到 Redis 当中获取(因为我们抢购成功直接// 将表单信息存储到了Redis 当中了。 key表示:order:userId:goodsId Value表示订单 seckillOrder),// 获取对应的秒杀订单,如果有,则说明该// 用户已经桥抢购了,每人限购一个SeckillOrder o = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" +goodsVo.getId()); // 因为我们在 Redis 当中的 value值就是 SeckillOrder 订单对象,所以这里可以直接强制类型转换if (null != o) { // 不为null,说明 Redis 存在该用户订单信息,说明该用户已经抢购了该商品model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中return "secKillFail"; // 返回一个错误页面}// Redis库存预减,如果在 Redis 中预减库存,发现秒杀商品已经没有了,就直接返回// 从面减少去执行 orderService.seckill()请求,防止线程堆积,优化秒杀/高并发// 提示: Redis 的 decrement是具有原子性的,已经存在了原子性,就是一条一条执行的,不会存在,复购,多购的可能性。// 注意:这里我们要操作的 key 的是:seckillGoods:商品Id,value:该商品的库存量Long decrement = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);if (decrement < 0) { // 说明这个商品已经没有库存了,返回// 这里我们可以恢复库存为 0 ,因为后面可能会一直减下去,恢复为 0 让数据更好看一些redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage()); // 将错误信息返回给下一页的模板当中return "secKillFail"; // 返回一个错误页面}// 抢购Order order = orderService.seckill(user, goodsVo);if (order == null) { // 说明抢购失败了,由于什么原因model.addAttribute("errmsg", RespBeanEnum.ENTRY_STOCK.getMessage());return "secKillFail"; // 返回一个错误页面}// 走到这里,说明抢购成功了,将信息,通过 model 返回给页面model.addAttribute("order", order);model.addAttribute("goods", goodsVo);System.out.println("秒杀 V 2.0 ");return "orderDetail"; // 进入到订单详情页}/*** InitializingBean 接口当中的 afterPropertiesSet 表示项目启动就自动给执行该方法当中的内容* 该方法是在类的所有属性,都是初始化后,自动执行的* 这里我们就可以将所有秒杀商品的库存量,加载到 Redis 当中** @throws Exception*/@Overridepublic void afterPropertiesSet() throws Exception {// 获取所有可以秒杀的商品信息List<GoodsVo> list = goodsService.findGoodsVo();// 先判断是否为空if (CollectionUtils.isEmpty(list)) {return;}// 遍历 List,然后将秒杀商品的库存量,放入到 Redis// key:秒杀商品库存量对应 key:seckillGoods:商品Id,value:该商品的库存量list.forEach(goodsVo -> {redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());});