- 下单请求压测
- 创建测试计划
- 线程属性: 线程数200, ramp-up时间:10, 循环次数:20
- 压测结果: 平均值3617ms,95值4640ms,TPS吞吐量47.4/sec
- 交易性能瓶颈分析
-
交易验证完全依赖于数据库
-
库存行锁(修改库存时对item_id值所在行加行锁)
<!-- 库存扣减操作--> <update id="decreaseStock"> update item_stock set stock = stock - #{amount,jdbcType=INTEGER} where item_id = #{itemId,jdbcType=INTEGER} and stock >= #{amount} </update> </mapper>
-
后置处理逻辑(要进行6次左右的数据库操作)
// 获取商品信息(包括下面的获取库存信息和获取活动商品信息) ItemModel itemModel = itemService.getItemById(itemId); // 查询数据库1: 获取库存数量 ItemStockDO itemStockDO = itemStockDOMapper.selectByItemId(itemDO.getId()); // 查询数据库2: 获取活动商品信息 PromoModel promoModel = promoService.getPromoByItemId(itemModel.getId()); // 查询数据库3: 获取用户信息 UserModel userModel = userService.getUserById(userId); // 数据库操作4: 减库存 boolean result = itemService.descreaseStock(itemId, amount); // 数据库操作5: 订单入库 orderDOMapper.insertSelective(orderDO); // 数据库操作6: 增加销量 itemService.increaseSales(itemId, amount);
- 交易验证优化策略
- 用户风控策略优化: 策略缓存模型化
获取用户信息是为了进行风控,还有判断是否异地登陆、是否最近修改密码等风控. - 活动校验策略优化: 引入活动发布流程,模型缓存化,紧急下线能力
- 用户信息与商品信息缓存代码
-
ItemService.interface
UserModel getUserByIdInCache(Integer id);
-
ItemServiceImpl.java
/** * item及promo model缓存模型 */ public ItemModel getItemByIdInCache(Integer id){ ItemModel itemModel = (ItemModel)redisTemplate.opsForValue().get("item_validate_" + id); if(itemModel == null){ itemModel = this.getItemById(id); redisTemplate.opsForValue().set("item_validate_" + id, itemModel); redisTemplate.expire("item_validate_" + id, 10, TimeUnit.MINUTES); } return itemModel; }
-
OrderServiceImpl.java
public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount) throws BusinessException { // 1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确 // ItemModel itemModel = itemService.getItemById(itemId); // 替换为使用缓存的方式 ItemModel itemModel = itemService.getItemByIdInCache(itemId);
-
本地测试
- 执行代码后可以调试OrderServiceImpl和ItemServiceImpl文件,看是否能够从缓存中获取用户与商品信息.
- redis缓存信息如下
-
代码部署到应用服务器
-
分布式环境下jmeter压测: 平均值2152ms, 95值2930ms, TPS: 72.7/sec
可见,与没有使用用户信息、商品信息缓存相比有了较大提升. -
redis查看是否用户信息和商品信息放入缓存中
- 库存行锁优化
-
扣减库存缓存化
- 方案: 活动发布同步库存进缓存、下单交易减缓存库存
- 活动发布同步库存进缓存核心代码
- PromoServiceImpl.java(省略PromoService接口中的方法声明)
@Resource private ItemService itemService; @Resource private RedisTemplate redisTemplate; @Override public void publishpromo(Integer promoId) { // 通过活动id获取活动 PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId); if(promoDO.getItemId() == null || promoDO.getItemId() == 0){ return; } ItemModel itemModel = itemService.getItemById(promoDO.getItemId()); // 将库存同步到redis中 redisTemplate.opsForValue().set("promo_item_stock_" + itemModel.getId(), itemModel.getStock()); }
- ItemController.java
@Resource private PromoService promoService; /** * 发布促销活动:将库存同步到redis中 * @param id * @return */ @GetMapping(value = "/publishpromo") public CommonReturnType publishPromo(@RequestParam("id") Integer id){ promoService.publishpromo(id); return CommonReturnType.create(null); }
- 本地缓存测试
- 查看本地redis
- PromoServiceImpl.java(省略PromoService接口中的方法声明)
- 下单交易减缓存库存核心代码
- ItemServiceImpl.java
public boolean descreaseStock(Integer itemId, Integer amount) throws BusinessException { // int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount); // 减缓存中的库存操作,返回剩余库存数量 long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount * -1); if(result >= 0){ // 更新库存成功 return true; }else{ // 更新库存失败 return false; } }
- 扣减库存测试
item_id原库存为9982
下单后redis中的库存如下,为9981:
- ItemServiceImpl.java
- 目前存在的问题
redis中的库存和数据库中的不一致. - 解决方法: 使用异步消息队列rocketmq.
-
异步写入数据库
-
库存数据库最终一致性保证(下一个博客中讲)
- rocketmq的概念、安装、使用
- rocketmq异步写入数据库代码编写
-
pom.xml文件引入rocketmq
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.3.0</version> </dependency>
-
application.yml文件变量引入
mq: nameserver: addr: rocketmq服务器ip:9876 topicname: stock
-
开放rocketmq服务器9876、10911端口
# 9876端口为nameserver默认端口,10911端口为broker默认端口 firewall-cmd --add-port=9876/tcp --permanent firewall-cmd --add-port=10911/tcp --permanent firewall-cmd --reload firewall-cmd --list-all
-
MqProducer.java(rocketmq消息生产者)
package com.kenai.mq; import com.alibaba.fastjson.JSON; import com.sun.org.apache.xpath.internal.operations.Bool; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @Component public class MqProducer { private DefaultMQProducer producer; @Value("${mq.nameserver.addr}") private String nameAddr; @Value("${mq.topicname}") private String topicName; @PostConstruct public void init() throws MQClientException { // 做mq producer的初始化 producer = new DefaultMQProducer("product_group"); // producer连接nameserver producer.setNamesrvAddr(nameAddr); producer.start(); } // 同步库存扣减消息 public Boolean asyncReduceStock(Integer itemId, Integer amount){ Map<String, Object> bodyMap = new HashMap<>(); bodyMap.put("itemId", itemId); bodyMap.put("amount", amount); Message message = new Message(topicName, "increase", JSON.toJSON(bodyMap).toString().getBytes(StandardCharsets.UTF_8)); try { producer.send(message); } catch (MQClientException e) { e.printStackTrace(); return false; } catch (RemotingException e) { e.printStackTrace(); return false; } catch (MQBrokerException e) { e.printStackTrace(); return false; } catch (InterruptedException e) { e.printStackTrace(); return false; } return true; } }
-
ItemServiceImpl.java(减库存、发消息)
@Override @Transactional public boolean descreaseStock(Integer itemId, Integer amount) throws BusinessException { // int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount); // 减缓存中的库存操作,返回剩余库存数量 long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount * -1); if(result >= 0){ // 发送消息 boolean mqResult = mqProducer.asyncReduceStock(itemId, amount); // 发送消息失败,则将库存补回去,返回false if(!mqResult){ redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount); return false; } return true; }else{ // 更新库存失败,将库存补回去 redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount); return false; } }
-
MqConsumer.java(rocketmq消息消费者)
package com.kenai.mq; import com.alibaba.fastjson.JSON; import com.kenai.dao.ItemStockDOMapper; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.List; import java.util.Map; @Component public class MqConsumer { private DefaultMQPushConsumer consumer; @Resource private ItemStockDOMapper itemStockDOMapper; @Value("${mq.nameserver.addr}") private String nameAddr; @Value("${mq.topicname}") private String topicName; @PostConstruct public void init() throws MQClientException { consumer = new DefaultMQPushConsumer("stock_consumer_group"); // consumer连接nameserver consumer.setNamesrvAddr(nameAddr); // consumer订阅所有stock topic消息 consumer.subscribe(topicName, "*"); // 当消息推送过来之后的处理方式 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) { // 实现库存真正到数据库内扣减的逻辑 Message msg = msgs.get(0); String jsonString = new String(msg.getBody()); Map<String, Object> map = JSON.parseObject(jsonString, Map.class); Integer itemId = (Integer) map.get("itemId"); Integer amount = (Integer) map.get("amount"); itemStockDOMapper.decreaseStock(itemId, amount); // 说明该消息已经被消费 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); } }
-
结果验证
- 下单
- 查看redis中缓存的库存数量
- 查看数据库中库存数量
- 总结
redis缓存库存+使用rocketmq异步写入数据库操作成功
- 下单
- 分布式环境redis库存缓存+使用rocketmq异步写入数据库测试
- jar包部署
- 启动应用服务器、nginx服务器、redis/rocketmq/mysql服务器
- 发布秒杀商品
- 查看redis中商品库存缓存信息
- 秒杀商品下单测试
- 查看redis中商品库存缓存信息
- 查看数据库中商品库存信息
- 可见redis中缓存商品库存和mysql数据库中的库存一致
- 使用jmeter分布式环境下单测试
-
线程属性值设置
-
redis中库存缓存数据查看
-
mysql数据库中数据查看
可见redis中库存缓存数据和mysql数据库数据一致 -
jmeter测试结果
-
压测原因分析
- 服务器为1核2G
- mysql数据库服务器、redis服务器、rocketmq服务器共用一台服务器,压力比较大.
- 目前存在的问题
- 异步写入数据库消息发送失败
当前的处理方式是回滚,应该使用更好的处理方式 - 库存扣减/回补操作执行失败
- 下单失败无法正确回补库存
下单操作在减库存操作后,如果减库存操作成功但是下单操作失败,由于redis不会回滚,rocketmq的消息也成功消费掉了,则会出现多减库存的情况发生.