本篇博客介绍了下单交易的性能优化技术,通过交易验证缓存的优化,库存缓存模型优化解决了交易流程中繁琐耗性能的验证缓存,并解决数据库库存行锁的问题,同时也引入了缓存与数据库分布式提交过程中不一致的风险。
本章的学习目标是:
验收效果
jmeter验收验证优化效果
Average time = 600ms tps=1200/s
itemId需要创建唯一索引
alter table item_stock add unique index
item_id_index(item_id)
方案:
问题:
数据库记录不一致,缓存中修改了但是数据库中的数据没有进行修改;
方案:
可以让C端用户完成购买商品的高效体验,又能保证数据库最终的一致性;
rocketmq:
RocketMQ原理
Producer解决消息生产的问题,Consumer消息的消费端
Broker
相当于一个中间人,由topic和MessageQueue组成,任何一条rocketmq的消息都是隶属于某一个topic,一个topic可以被一个messagebroker管理,也可以被多个messagebroker管理;
Broker向Nameserver发送注册请求,broker的ip和负责的topic,queue;
每一个broker至少有一个queue,producer从Nameserver上发现broker1;
采用负载轮询的方式第一次请求到queue1中,生成一个message1,第二次请求到queue2,生成一个message2,同时consumer会向这两个queue分别建立长连接。
当producer做对应投递时,consumer会被唤醒,拉取对应的message,这种方式被称为长轮询;
Consumer group的作用:以queue为单位作为一个消息的管理,当consumer消费完一个message的时候会回复一个消息给对应的queue,并且将对应的message2变为已经消费成功要被干掉的状态;
若对应的一个queue被多个consumer消费的情况下,势必会造成一个同步的问题,存在一个锁竞争的机制,rocketmq采用的是以queue为单位平均的分配给consumer,所以设计一个好的中间件就是为了保证queue和consumer的数量相等;
当有多个consumer group,一个订单系统就属于一个group,另外一个consumer group就属于商品系统;生产者端并不知道生成的消息对应的消费端是哪个系统,只会无脑的投消息,关注这个消息的人指出来即可。topicA还是以consumer_group为管理的基础单元,一个queue1可以被一个consumer group中的一个consumer所消费,也可以被另一个consumer group中的consumer所消费,以consumer_group去做对应消息的消费和管理。
如果对应的broker1产生了任何异常,producer知道broker掉线了,没办法投递对应的消息;
broker2作为broker1的slave,平时不对外进行服务,只做消息的从库,一旦对应的message1被消费;一旦broker1发生异常,nameserver感知到会将broker2变为主库,并且通知producer和consumer端,让其通过broker2去接管对应的消费,slave和master之间的数据可以是同步,也可以是异步。
同步的话,producer生成在broker1中生产message1成功,也要broker2中备份message1也能够成功,性能偏低;
主broker1作为生产成功\消费成功即可,roker2做异步复制即可。只要网络的延迟小,对应的cpu处理速度快,是不会发生消息丢失的情况。但是在分布式的环境下,没有办法同时保证强一致性和可用性。如果选择强可用性肯定会降低强一致性,当发生主备切换的时候可能会发生消息的丢失。
之前所有的操作都是在单库上面去执行的,依赖于spring的transactional标签,借助于MySQL数据库的ACID****(原子性、一致性、隔离性、持久性)对应的刚性事务强一致的方式保证了数据库事务的一致性问题。
分布式设计CAP三方面,一致性、可用性、分区容忍性
分区容忍性是必要的,要么选择强一致性,等待所有的数据都一致的时候才可用;要么就是牺牲强一致性变得可用。所以牺牲强一致性来实现CAP中的A和P(可用性和分区容忍性)。强一致性是重要的,但是不追求瞬时状态的强一致性,追求的是最终的一致性,达到基础可用、最终一致性、软状态;
软状态:在应用当中会瞬时的存在有数据不一致性的情况,比如一部分数据已经成功,另外一部分数据还在处理当中。那我们的业务认为这些是可以容忍的;
在我们的缓存库存中,redis中存储的状态都是正确的,但是由于异步消息队列的consumer没有被触发,在那一瞬时数据库的状态是错误的。但只要分布式事务的消息投递成功,数据库的状态就会被正确更新,这个设计就是用来处理库存最终一致性的方案。只要消息中间件有99%以上的高可用的方式,就有99%以上的概率是可以保证数据库的状态可以跟redis中的状态是一致的。
rocketmq安装地址
修改JAVA_OPT
JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn512m"
./mqadmin updateTopic -n localhost:9876 -t stock -c DefaultCluster
//完成topic的创建就可以使用该创建的topic
新建一个mq的package;
新建一个MqProducer.java和MqConsumer.java;
在application.properties
mq.nameserver.addr=115.28.67.199:9876 //nameserver地址和端口
mq.topicname=TopicTest //topicname
//进入poxm.xml接入rocketmq的jar包
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
MqProducer.java
MqConsumer.java
ItemServiceImpl.java
一旦更新库存成功,发送一条消息出去让异步消息队列感知到用来减数据库的库存
@Autowired
private MqProducer mqProducer;
@Override
@Transactional
public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
//int affectedRow = itemStockDOMapper.decreaseStock(itemId,amount);
long result = redisTemplate.opsForValue().increment("promo_item_stock_"+itemId,amount.intValue() * -1);
if(result >0){
//更新库存成功
return true;
}else if(result == 0){
//打上库存已售罄的标识
redisTemplate.opsForValue().set("promo_item_stock_invalid_"+itemId,"true");
//更新库存成功
return true;
}else{
//更新库存失败
increaseStock(itemId,amount);
return false;
}
}