秒杀商品
这里才是我们的重头戏这里我们主要讲解使用思路,不过多的去展示无用代码如实体类等,我们这里从最开始的
直接处理
redis 事务处理
分布式锁 Lua处理
三种方式 由浅至深的来理解秒杀的思路和超卖问题的解决
直接处理
判断用户id 的有效性 我们没有用户
判断goodsid的有效性
判断当前是否处于可以秒杀的状态
判断是否有剩余库存
判断用户的秒杀权限(是否秒杀过)
减少库存
生成新的订单
public String handle(String userId, String goodsId) { String stockkey = goodsId + "_count"; String startTimekey = goodsId + "_startTime"; String userkey = goodsId + "_" + userId; String dateStr = (String) redisUtil.get(startTimekey); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date startTime = null; try { startTime = format.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } if (startTime == null || new Date().before(startTime)) { return "秒杀还没开始"; } int stockNum = (int) redisUtil.get(stockkey); if (stockNum <= 0) { return "秒杀一空"; } if (redisUtil.get(userkey) != null) { return "用户秒杀成功过了"; } //减少库存 redisUtil.decr(stockkey); //限额 1 可以这么处理 //如果限额 多个 可以获取value 自增 redisUtil.set(goodsId + "_" + userId, 1); return userId + "用户秒杀成功"; }
测试
这个时候启动 jmeter 来测试一下接口的结果
库存 10
一百个线程 抢这 10个 手机
查看 redis 中 库存 key 的数量 为 -4
再次测试
通过测试和查看日志可以看到,我们直接处理,系统不行不能在第一时间反应过来是否超过了库存,导致特价商品被超额卖出,那这个问题怎么解决呢?
事务处理
优秀成熟的数据库 一定会有对事务的支持, redis 也不例外
Redis的事务在不出现异常的时候是原子操作,exec是触发事务执行的命令
相关命令:
watch 设置一个key 表示开始对这个key的命令监听
multi 将 后续的命令 加入到执行队列里
exec 出发执行队列里的命令
discard 停止提交,redis不能回滚
使用事务
public String handleByTrans(String userId, String goodsId) { String stockkey = goodsId + "_count"; String startTimekey = goodsId + "_startTime"; String userkey = goodsId + "_" + userId; String dateStr = (String) redisUtil.get(startTimekey); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date startTime = null; try { startTime = format.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } if (startTime == null || new Date().before(startTime)) { return "秒杀还没开始"; } if (redisUtil.get(userkey) != null) { return "用户秒杀成功过了"; } SessionCallback sessionCallback = new SessionCallback() { @Override public Object execute(RedisOperations redisOperations) throws DataAccessException { // 编写事务逻辑 // 调用 watch 开始事务, mutil 开始将之后的命令加入队列 exec 执行队列里的命令 redisOperations.watch(stockkey); int stockNum = (int) redisUtil.get(stockkey); if (stockNum <= 0) { return "秒杀一空"; } redisOperations.multi(); redisUtil.decr(stockkey); redisUtil.set(userkey, 1); return redisOperations.exec(); } }; redisUtil.execute(sessionCallback); if (redisUtil.hasKey(userkey)) { return userId; } return userId + "秒杀失败"; }
测试
和之前一样的案例 100 抢 10
可以发现使用了 事务之后我们解决了超卖的问题
举一反三
那除了事务 还有什么方式可以解决超卖问题呢?
脚本语言 : lua
redis是支持执行脚本语言的且一段脚本执行的时候是不会被其他操作影响的,保证原子性
编写lua 脚本
--接受到参数1 local userId = KEYS[1]; --接收到参数2 local goodsId = KEYS[2]; --redis中的 key 存储为 局部变量 --秒杀商品数量 --.. 是lua 里面字符串的拼接 local cntkey = goodsId .. "_count"; --秒杀用户 id local orderkey = goodsId .. "_" .. userId; --判断是否秒杀过 local orderExists = redis.call("get", orderkey); if (orderExists and tonumber(orderExists) == 1) then return 2; end --判断库存是否为空 local num = redis.call("get", cntkey); if (num and tonumber(num) <= 0) then return 0; else -- 秒杀成功 redis.call("decr", cntkey); redis.call("set", orderkey, 1); end return 1
实例代码
我们可以使用 DefaultRedisScript 类 来执行我们写在 resouce 下的lua脚本
script.setScriptSource(new ResourceScriptSource( new ClassPathResource("lua/sekill.lua")));
设置脚本的位置,之后用之前工具类封装的redis方法将脚本和需要的参数list 传入执行即可
//使用lua 脚本 保证原子性 (分布式锁) @Override public String handleByLua(String userId, String goodsId) { String startTimekey = goodsId + "_startTime"; String dateStr = (String) redisUtil.get(startTimekey); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date startTime = null; try { startTime = format.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } if (startTime == null || new Date().before(startTime)) { return "秒杀还没开始"; } DefaultRedisScript<Long> script = new DefaultRedisScript<>(); //设置返回脚本类型 script.setResultType(Long.class); script.setScriptSource(new ResourceScriptSource( new ClassPathResource("lua/sekill.lua"))); List<String> keylist = new ArrayList<>(); keylist.add(userId); keylist.add(goodsId); Object execute = redisUtil.execute(script, keylist); String resultStr = String.valueOf(execute); if ("0".equals(resultStr)) { return "秒杀库存已经空了"; } else if ("1".equals(resultStr)) { return userId; } else if ("2".equals(resultStr)) { return "该用户秒杀过了 =>" + userId; } else { return "秒杀和抢购可能出了点问题,正在紧急维护"; } }
测试
同样的 100 -》 10 ,从结果来看,也是保证了不超卖,不少卖的
解决了秒杀的问题,剩下就是将秒杀之后的成功用户信息和商品信息传给订单模块即可
Controller
@Controller @Slf4j public class redisController { @Autowired redisService redisService; @Autowired RedisUtil redisUtil; // 为了 方面测试 每次重启项目都会重置秒杀次数 @PostConstruct public void initdata() { redisService.initdata(); } @PostMapping(value = "/seckillAPI") public String seckill(String userId, String goodsId, Model model) { String key = goodsId + "_" + userId; if (userId == null || goodsId == null) { return "参数异常"; } String result = redisService.seckill(userId, goodsId); if (redisUtil.hasKey(key)) { log.info(result); model.addAttribute("userId", result); model.addAttribute("goodsId", goodsId); } return "order"; } }
生成订单
解决了秒杀的问题,生成订单其实就简单和常规的不少
就是查询 必要的信息生成一个订单信息
service 实现类
@Service public class orderServiceImpl implements orderService { @Autowired GoodsMapper goodsMapper; @Autowired SeckillGoodsMapper seckillGoodsMapper; @Autowired orderMapper ordermapper; @Override public orderVo createOrder(order order) { int insert = ordermapper.insert(order); if (insert != 0) { List<order> orders = ordermapper.selectList(null); for (order o : orders) { goods goods = goodsMapper.getGoodsByGoodsId(o.getGoods_id()); SeckillGoods seckillGoods = seckillGoodsMapper.getSeckillByGoodsId(o.getGoods_id()); orderVo orderVo = new orderVo(); orderVo.setOrderId(String.valueOf(o.getId())); orderVo.setUserId(o.getUser_id()); orderVo.setGoodsId(o.getGoods_id()); orderVo.setTelephone(o.getTelephone()); orderVo.setAddress(o.getAddress()); orderVo.setImgPath(goods.getImg_path()); orderVo.setSeckillPrice(seckillGoods.getSeckill_price()); return orderVo; } } return null; } }
Controller
等待用户 确认信息之后 就可以生成订单 同步到数据库了
@Controller public class orderController { @Autowired orderService orderService; @PostMapping(value = "/create") public String createOrder(order order, Model model) { orderVo order1 = orderService.createOrder(order); model.addAttribute("orderinfo", order1); return "success"; } }
流程展示
总结
设计一个秒杀项目 其实要考虑的东西十分的多,我们这次的系统也不是最终的版本,先做出来的核心的,
套用鱼皮的话 先有 再调优 追求更好
拓展
页面动静分离
nginx ip 分流
MQ 流量削峰,异步任务
前端验证码
数据库与缓存同步策略(MQ redis 都可以实现)