公司自己买服务器建设网站,湖北住房和城乡建设厅官方网站,网站首页轮播,吴江建网站优荐苏州聚尚网络秒杀高并发解决方案
1.秒杀/高并发方案-介绍
秒杀/高并发 其实主要解决两个问题#xff0c;一个是并发读#xff0c;一个是并发写并发读的核心优化理念是尽量减少用户到 DB 来读数据#xff0c;或者让他们读更少的数据, 并 发写的处理原则也一样针对秒杀系统需…秒杀高并发解决方案
1.秒杀/高并发方案-介绍
秒杀/高并发 其实主要解决两个问题一个是并发读一个是并发写并发读的核心优化理念是尽量减少用户到 DB 来读数据或者让他们读更少的数据, 并 发写的处理原则也一样针对秒杀系统需要做一些保护针对意料之外的情况设计兜底方案以防止最坏的情况 发生。系统架构要满足高可用: 流量符合预期时要稳定要保证秒杀活动顺利完成即秒杀商品 顺利地卖出去这个是最基本的前提系统保证数据的一致性: 就是秒杀 10 个 商品 那就只能成交 10 个商品多一个少一 个都不行。一旦库存不对就要承担损失系统要满足高性能: 也就是系统的性能要足够高需要支撑大流量, 不光是服务端要做极 致的性能优化而且在整个请求链路上都要做协同的优化每个地方快一点, 整个系统就快 了秒杀涉及大量的并发读和并发写因此支持高并发访问这点非常关键, 对应的方案比如页 面缓存方案、Redis 预减库存/内存标记与隔离、请求的削峰(RabbitMQ/异步请求)、分布式 Session 共享等 2.秒杀场景模拟 这里模拟每秒1000个请求循环10次即总共发起10000次请求请求/seckill/doSeckill接口
3.秒杀解决方案
3.1 v1.0-原始版本
SeckillController
Controller
RequestMapping(value /seckill)
public class SeckillController {Resourceprivate GoodsService goodsService;Resourceprivate SeckillOrderService seckillOrderService;Resourceprivate OrderService orderService;RequestMapping(value /doSeckill)public String doSeckill(Model model, User user, Long goodsId) {System.out.println(-----秒杀 V1.0--------);//秒杀 v1.0 start if (user null) {return login;}model.addAttribute(user, user);GoodsVo goodsVo goodsService.findGoodsVoByGoodsId(goodsId);//判断库存if (goodsVo.getStockCount() 1) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}//解决重复抢购LambdaQueryWrapperSeckillOrder seckillOrderLambdaQueryWrapper new QueryWrapperSeckillOrder().lambda().eq(SeckillOrder::getGoodsId, goodsId).eq(SeckillOrder::getUserId, user.getId());SeckillOrder seckillOrder seckillOrderService.getOne(seckillOrderLambdaQueryWrapper);if (seckillOrder ! null) {model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}//抢购Order order orderService.seckill(user, goodsVo);if (order null) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}model.addAttribute(order, order);model.addAttribute(goods, goodsVo);return orderDetail;//秒杀 v1.0 end... }}OrderServiceImpl
Service
public class OrderServiceImpl extends ServiceImplOrderMapper, Order implements OrderService {Resourceprivate SecKillGoodsService secKillGoodsService;Resourceprivate OrderMapper orderMapper;Resourceprivate SeckillOrderMapper seckillOrderMapper;/*** 秒杀商品减少库存* param user* param goodsVo* return*/Overridepublic Order seckill(User user, GoodsVo goodsVo) {//1.根据商品id获取秒杀商品信息LambdaQueryWrapperSecKillGoods lambdaQueryWrapper new QueryWrapperSecKillGoods().lambda().eq(SecKillGoods::getGoodsId,goodsVo.getId());SecKillGoods seckillGoods secKillGoodsService.getOne(lambdaQueryWrapper);//2.秒杀商品库存-1seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);secKillGoodsService.updateById(seckillGoods);//3.生成普通订单Order order new Order();order.setUserId(user.getId());order.setGoodsId(goodsVo.getId());order.setDeliveryAddrId(0L);order.setGoodsName(goodsVo.getGoodsName());order.setGoodsCount(1);order.setGoodsPrice(seckillGoods.getSeckillPrice());order.setOrderChannel(1);order.setStatus(0);order.setCreateDate(new Date());orderMapper.insert(order);//4.生成秒杀订单SeckillOrder seckillOrder new SeckillOrder();seckillOrder.setGoodsId(goodsVo.getId());seckillOrder.setUserId(user.getId());seckillOrder.setOrderId(order.getId());seckillOrderMapper.insert(seckillOrder);return order;}
}说明
非高并发情况下程序执行流程分析 程序首先校验用户是否登录 然后判断商品库存是充足 在判断是否存在重复抢购 最后执行抢购生成订单及抢购订单数据
高并发情况下,程序执行流程分析
判断商品库存是充足、否存在重复抢购的程序是不具备原子性秒杀商品的库存为负数但具体的值无法确定因为获取秒杀商品库存也不具备原子性当有多少个请求冲破了前面库存和抢购的过滤就会去生成多少个订单
3.2 v2.0-秒杀-复购、超卖处理
SeckillController
RequestMapping(value /doSeckill)
public String doSeckill(Model model, User user, Long goodsId) {System.out.println(-----秒杀 V2.0--------);//秒杀 v2.0 start if (user null) {return login;}model.addAttribute(user, user);GoodsVo goodsVo goodsService.findGoodsVoByGoodsId(goodsId);//判断库存if (goodsVo.getStockCount() 1) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}//解决重复抢购,直接到redis中获取对应的秒杀订单如果有则说明已经抢购了不能继续抢购SeckillOrder seckillOrder (SeckillOrder) redisTemplate.opsForValue().get(order: user.getId() : goodsId);if (seckillOrder ! null) {model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}//抢购Order order orderService.seckill(user, goodsVo);if (order null) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}model.addAttribute(order, order);model.addAttribute(goods, goodsVo);return orderDetail;//秒杀 v2.0 end...
}OrderServiceImpl
/*** 秒杀商品减少库存** param user* param goodsVo* return*/
Override
Transactional(rollbackFor {Exception.class})
public Order seckill(User user, GoodsVo goodsVo) {//1.根据商品id获取秒杀商品信息LambdaQueryWrapperSecKillGoods lambdaQueryWrapper new QueryWrapperSecKillGoods().lambda().eq(SecKillGoods::getGoodsId, goodsVo.getId());SecKillGoods seckillGoods secKillGoodsService.getOne(lambdaQueryWrapper);//2.秒杀商品库存-1// seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);// secKillGoodsService.updateById(seckillGoods);//1. Mysql在默认的事务隔离级别[REPEATABLE-READ]下//2. 执行update语句时,会在事务中锁定要更新的行//3. 这样可以防止其它会话在同一行执行update,deleteLambdaUpdateWrapperSecKillGoods updateWrapper new UpdateWrapperSecKillGoods().lambda().setSql(stock_countstock_count-1).eq(SecKillGoods::getGoodsId, goodsVo.getId()).gt(SecKillGoods::getStockCount, 0);boolean update secKillGoodsService.update(updateWrapper);//如果更新失败说明已经没有库存了if (!update) {return null;}//3.生成普通订单Order order new Order();order.setUserId(user.getId());order.setGoodsId(goodsVo.getId());order.setDeliveryAddrId(0L);order.setGoodsName(goodsVo.getGoodsName());order.setGoodsCount(1);order.setGoodsPrice(seckillGoods.getSeckillPrice());order.setOrderChannel(1);order.setStatus(0);order.setCreateDate(new Date());orderMapper.insert(order);//4.生成秒杀订单SeckillOrder seckillOrder new SeckillOrder();seckillOrder.setGoodsId(goodsVo.getId());seckillOrder.setUserId(user.getId());seckillOrder.setOrderId(order.getId());seckillOrderMapper.insert(seckillOrder);//设置秒杀订单的key -- order:用户id:商品idredisTemplate.opsForValue().set(order: user.getId() : goodsVo.getId(), seckillOrder);return order;
}说明
超卖问题处理 前面出现超卖的主要原因在于更新库存时不具备原子性这里利用mysql默认的事务隔离级别
[REPEATABLE-READ]在事务中执行update语句获取锁定更新的行这个机制进行处理
复购生成了多个订单问题处理则在生成订单之前对库存更新进行判断如果更新失败则不在生成订单 //如果更新失败说明已经没有库存了if (!update) {return null;}优化复购判断 在生成秒杀订单之后将订单存储到redis中在判断复购时直接从redis中获取避免频繁查询数据库提升执行效率 //解决重复抢购,直接到redis中获取对应的秒杀订单如果有则说明已经抢购了不能继续抢购SeckillOrder seckillOrder (SeckillOrder) redisTemplate.opsForValue().get(order: user.getId() : goodsId);if (seckillOrder ! null) {model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}测试结果 3.3 v3.0 优化秒杀Redis 预减库存
前面我们防止超卖 是通过到数据库查询和到数据库抢购来完成的, 代码如下: 如果在短时间内大量抢购冲击 DB, 造成洪峰, 容易压垮数据库解决方案: 使用 Redis 完成预减库存如果没有库存了, 直接返回, 减小对 DB 的压力示意图 代码实现
Controller
RequestMapping(value /seckill)
public class SeckillController implements InitializingBean {Resourceprivate GoodsService goodsService;Resourceprivate SeckillOrderService seckillOrderService;Resourceprivate OrderService orderService;Resourceprivate RedisTemplate redisTemplate;RequestMapping(value /doSeckill)public String doSeckill(Model model, User user, Long goodsId) {System.out.println(-----秒杀 V3.0--------);//秒杀 v3.0 start if (user null) {return login;}model.addAttribute(user, user);GoodsVo goodsVo goodsService.findGoodsVoByGoodsId(goodsId);//判断库存if (goodsVo.getStockCount() 1) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}//解决重复抢购,直接到redis中获取对应的秒杀订单如果有则说明已经抢购了不能继续抢购SeckillOrder seckillOrder (SeckillOrder) redisTemplate.opsForValue().get(order: user.getId() : goodsId);if (seckillOrder ! null) {model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}//库存预减如果在redis中预减库存发现秒杀商品中已经没有库存了直接返回//从而减少这个方法请求 orderService.seckill防止大量请求打到数据优化秒杀//这个方法具有原子性Long decrement redisTemplate.opsForValue().decrement(seckillGoods: goodsId);//说明这个商品已经没有库存了if (decrement 0) {//恢复redis库存为0,对业务没有影响只是为了好看redisTemplate.opsForValue().increment(seckillGoods: goodsId);model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}//抢购Order order orderService.seckill(user, goodsVo);if (order null) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}model.addAttribute(order, order);model.addAttribute(goods, goodsVo);return orderDetail;//秒杀 v3.0 end... }//该方法实在类的所有属性都初始化后自动执行//这里将所有秒杀商品的库存量加载到redis中Overridepublic void afterPropertiesSet() throws Exception {//1.查询所有的秒杀商品ListGoodsVo list goodsService.findGoodsVo();if (CollectionUtils.isEmpty(list)) {return;}//遍历list,将秒杀商品的库存量放入到redis中//秒杀商品库存量对应的key seckillGoods:商品idfor (GoodsVo goodsVo : list) {redisTemplate.opsForValue().set(seckillGoods: goodsVo.getId(), goodsVo.getStockCount());}}}大体思路 项目启动时加载秒杀商品库存到redis中库存量随着每次启动都会更新 在进行抢购之前先到redis中进行库存预减利用redis的decrement具备原子性的特点 如果redis预减库存小于0则直接返回避免了多余的请求打到数据库
3.4 v4.0 优化秒杀加入内存标记避免总到 Reids 查询库存
需求分析/图解
如果某个商品库存已经为空了, 我们仍然是到 Redis 去查询的, 还可以进行优化解决方案: 给商品进行内存标记, 如果库存为空, 直接返回, 避免总是到 Redis 查询库存分析思路-示意图 Controller
RequestMapping(value /seckill)
public class SeckillController implements InitializingBean {Resourceprivate GoodsService goodsService;Resourceprivate SeckillOrderService seckillOrderService;Resourceprivate OrderService orderService;Resourceprivate RedisTemplate redisTemplate;RequestMapping(value /doSeckill)public String doSeckill(Model model, User user, Long goodsId) {System.out.println(-----秒杀 V4.0--------);//秒杀 v4.0 start if (user null) {return login;}model.addAttribute(user, user);GoodsVo goodsVo goodsService.findGoodsVoByGoodsId(goodsId);//判断库存if (goodsVo.getStockCount() 1) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}//解决重复抢购,直接到redis中获取对应的秒杀订单如果有则说明已经抢购了不能继续抢购SeckillOrder seckillOrder (SeckillOrder) redisTemplate.opsForValue().get(order: user.getId() : goodsId);if (seckillOrder ! null) {model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}//如果库存为空避免总是到 reids 去查询库存给 redis 增加负担内存标记if (entryStockMap.get(goodsId)) {//如果当前这个秒杀商品已经是空库存则直接返回.model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}//库存预减如果在redis中预减库存发现秒杀商品中已经没有库存了直接返回//从而减少这个方法请求 orderService.seckill防止大量请求打到数据优化秒杀//这个方法具有原子性Long decrement redisTemplate.opsForValue().decrement(seckillGoods: goodsId);//说明这个商品已经没有库存了if (decrement 0) {//这里使用内存标记避免多次操作 redis, true 表示空库存了.entryStockMap.put(goodsId, true);//恢复redis库存为0,对业务没有影响只是为了好看redisTemplate.opsForValue().increment(seckillGoods: goodsId);model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}//抢购Order order orderService.seckill(user, goodsVo);if (order null) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}model.addAttribute(order, order);model.addAttribute(goods, goodsVo);return orderDetail;//秒杀 v4.0 end... }//该方法实在类的所有属性都初始化后自动执行//这里将所有秒杀商品的库存量加载到redis中Overridepublic void afterPropertiesSet() throws Exception {//1.查询所有的秒杀商品ListGoodsVo list goodsService.findGoodsVo();if (CollectionUtils.isEmpty(list)) {return;}//遍历list,将秒杀商品的库存量放入到redis中//秒杀商品库存量对应的key seckillGoods:商品idfor (GoodsVo goodsVo : list) {redisTemplate.opsForValue().set(seckillGoods: goodsVo.getId(), goodsVo.getStockCount());//当有库存为 false当无库存为 true。防止库存没有了还会到 Redis 进行判断操作entryStockMap.put(goodsVo.getId(), false);}}}3.4 v5.0 优化秒杀: 加入消息队列实现秒杀的异步请求
需求分析/图解
问题分析
前面秒杀, 没有实现异步机制, 是完成下订单后, 再返回的, 当有大并发请求下订单操作时, 数据库来不及响应, 容易造成线程堆积
解决方案 加入消息队列实现秒杀的异步请求 接收到客户端秒杀请求后服务器立即返回 正在秒杀中…, 有利于流量削峰 客户端进行轮询秒杀结果, 接收到秒杀结果后在客户端页面显示即可 秒杀消息发送设计 SeckillMessage - String 代码实现
秒杀消息类
Data
NoArgsConstructor
AllArgsConstructor
public class SeckillMessage {private User user;private Long goodsId;
}定义消息队列、交换机
Configuration
public class RabbitMQSecKillConfig {private static final String QUEUE seckillQueue;private static final String EXCHANGE seckillExchange;Beanpublic Queue queue_seckill() {return new Queue(QUEUE);}Beanpublic TopicExchange topicExchange_seckill() {return new TopicExchange(EXCHANGE);}Beanpublic Binding binding_seckill() {return BindingBuilder.bind(queue_seckill()).to(topicExchange_seckill()).with(seckill.#);}}发送秒杀消息
Service
Slf4j
public class MQSenderMessage {Resourceprivate RabbitTemplate rabbitTemplate;//发送秒杀信息public void senderMessage(String message) {System.out.println(发送消息了 message);log.info(发送消息 message);rabbitTemplate.convertAndSend(seckillExchange, seckill.message, message);}
}消息接收者
Service
Slf4j
public class MQReceiverConsumer {Resourceprivate GoodsService goodsService;Resourceprivate OrderService orderService;//下单操作RabbitListener(queues seckillQueue)public void queue(String message) {SeckillMessage seckillMessage JSONUtil.toBean(message, SeckillMessage.class);Long goodId seckillMessage.getGoodsId();User user seckillMessage.getUser();//获取抢购的商品信息GoodsVo goodsVo goodsService.findGoodsVoByGoodsId(goodId);//下单操作orderService.seckill(user, goodsVo);}}SeckillController
Controller
RequestMapping(value /seckill)
public class SeckillController implements InitializingBean {Resourceprivate GoodsService goodsService;Resourceprivate RedisTemplate redisTemplate;Resourceprivate MQSenderMessage mqSenderMessage;//如果某个商品库存已经为空, 则标记到 entryStockMapprivate HashMapLong, Boolean entryStockMap new HashMap();RequestMapping(value /doSeckill)public String doSeckill(Model model, User user, Long goodsId) {System.out.println(-----秒杀 V5.0--------);//秒杀 v5.0 start if (user null) {return login;}model.addAttribute(user, user);GoodsVo goodsVo goodsService.findGoodsVoByGoodsId(goodsId);//判断库存if (goodsVo.getStockCount() 1) {model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}//解决重复抢购,直接到redis中获取对应的秒杀订单如果有则说明已经抢购了不能继续抢购SeckillOrder seckillOrder (SeckillOrder) redisTemplate.opsForValue().get(order: user.getId() : goodsId);if (seckillOrder ! null) {model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}//如果库存为空避免总是到 reids 去查询库存给 redis 增加负担内存标记if (entryStockMap.get(goodsId)) {//如果当前这个秒杀商品已经是空库存则直接返回.model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());return secKillFail;}//库存预减如果在redis中预减库存发现秒杀商品中已经没有库存了直接返回//从而减少这个方法请求 orderService.seckill防止大量请求打到数据优化秒杀//这个方法具有原子性Long decrement redisTemplate.opsForValue().decrement(seckillGoods: goodsId);//说明这个商品已经没有库存了if (decrement 0) {//这里使用内存标记避免多次操作 redis, true 表示空库存了.entryStockMap.put(goodsId, true);//恢复redis库存为0,对业务没有影响只是为了好看redisTemplate.opsForValue().increment(seckillGoods: goodsId);model.addAttribute(errmsg, RespBeanEnum.REPEATE_ERROR.getMessage());return secKillFail;}//抢购// Order order orderService.seckill(user, goodsVo);// if (order null) {// model.addAttribute(errmsg, RespBeanEnum.ENTRY_STOCK.getMessage());// return secKillFail;// }// model.addAttribute(order, order);// model.addAttribute(goods, goodsVo);//抢购, 向 MQ 发消息, 因为不知道是否成功, 客户端需要轮询//errmsg 为排队中.... , 暂时返回 secKillFaill 页面SeckillMessage seckillMessage new SeckillMessage(user, goodsId);mqSenderMessage.senderMessage(JSONUtil.toJsonStr(seckillMessage));model.addAttribute(errmsg, 排队中.....);return orderDetail;//秒杀 v5.0 end... }//该方法实在类的所有属性都初始化后自动执行//这里将所有秒杀商品的库存量加载到redis中Overridepublic void afterPropertiesSet() throws Exception {//1.查询所有的秒杀商品ListGoodsVo list goodsService.findGoodsVo();if (CollectionUtils.isEmpty(list)) {return;}//遍历list,将秒杀商品的库存量放入到redis中//秒杀商品库存量对应的key seckillGoods:商品idfor (GoodsVo goodsVo : list) {redisTemplate.opsForValue().set(seckillGoods: goodsVo.getId(), goodsVo.getStockCount());//当有库存为 false当无库存为 true。防止库存没有了还会到 Redis 进行判断操作entryStockMap.put(goodsVo.getId(), false);}}}客户端轮询秒杀结果-思路分析示意图