秒杀 预约活动项目中如何高效的保证下单交易成功?保证redis( 三 )


3、最终直到redis中的库存减到0,停止售卖次商品 。
/**负责createOrder中的减库存操作*/private void orderDecreaseStock(Integer itemId, Integer amount) throws BusinessException {boolean result = itemService.decreaseStock(itemId, amount);if (!result) {throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);}}
@Override@Transactionalpublic boolean decreaseStock(Integer itemId, Integer amount) {Long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount.intValue() * -1);if (result >= 0) { // > 变 >= 再分化return true;}else {//库存为负,此次交易失败,补回redis中库存redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount.intValue());return false;}}
好处:因为redis是内存级别的效率非常高
坏处:redis和数据库中的数据不同步,如果redis宕机会导致用户购买商品的数量信息全部丢失 。简单来说就是缓存中的数据未能和数据库中的数据产生任何一致性,这种方法是不可取的
二、引入 1.思考为什么要引入MQ
因为要同步redis和Mysql中的数据同步 。
而MQ的左右就是在扣除redis中的stock之后,发送消息给Mysql让数据库扣减库存,异步更新Mysql保证redis和Mysql的最终一致性 。
关于的一般使用和简单概念,可看我这篇博客:
但是每当引入了一个新的结构,系统变得复杂了,此时就需要考虑这个下单过程中会出现那些问题,采取什么样的结构 。
2.下单采取的结构
第一种结构采用同步异步更新数据库
采用同步方式发送更新数据库消息,必定等到成功发送消息给才能算订单成功,返回给前端 。
第二种结构采用的是前端轮询加后端假异步
采用异步发送方式,并不需要等待消息是否发送成功,但是前端不能直接返回给用户,说下单成功,前端采用长轮询方式不断的访问后端是都更新数据库成功了,只有后端都成功才会发给前端消息,所以也称之为假异步 。
本项目中我们采用的是第一种结构
2.MQ遇到的问题
MQ发送更新数据库扣减库存的消息,应该在校验环节和订单生成环节都完成之后,在进行MQ的发送 。因为中的@只能回滚Mysql并不能回滚MQ发送的消息 。如果MQ先发送消息,之后订单方法出错了,此时就会白白的在Mysql中扣除一个库存导致,订单少买
如何解决这个问题呢?
使用事务机制 。
【秒杀预约活动项目中如何高效的保证下单交易成功?保证redis】3.事务机制
事务之正常事务过程
可以采用中的事务发送,将校验和生成订单放在的本地事务来做,只有本地事务成功了,才会发送这条消息给,本地事务失败了,MQ就不会发送消息给,整个订单就会回滚 。
事务之正常补偿过程
假设在创建订单这个过程发生了很长的时间,创建订单在数据库压力比较大的情况下可能用了10s多 。这样MQ就发现本地事务一直没有说提交成功还是提交失败回滚,处于一个的状态于是就需要n方法回调判断是否下单是否是成功的 。
我们需要创建一个新的数据表流水日志表表 。
当前中的决定当前订单的状态,2代表成功,3代表回滚,1代表不知道 。
每次当订单生成之后改动当前这条流水的状态,决定是否补偿结果 。
代码
@Componentpublic class MqConsumer {private DefaultMQPushConsumer consumer;@Value("${mq.nameserver.addr}")private String nameAddr;@Value("${mq.topicname}")private String topicName;@Resourceprivate ItemStockDOMapper itemStockDOMapper;@PostConstructpublic void init() throws MQClientException {consumer = new DefaultMQPushConsumer("stock_consumer_group");consumer.setNamesrvAddr(nameAddr);//订阅所有topicName的消息consumer.subscribe(topicName,"*");//consumer处理过程consumer.registerMessageListener(new MessageListenerConcurrently() {@Overridepublic ConsumeConcurrentlyStatus consumeMessage(List