一、基本思路
数据库有一张商品表,库存量是100。现在有1000个消费者准备开抢这100个库存。
t_product表维护商品编号与商品库存剩余数量。编号No123321的这种商品的库存量有100个。
t_product_record维护抢到商品的用户ID。理论上t_product表开抢后的 记录数量应该是100条(共有100个人抢到了商品)。
我们使用线程池创建1000个线程,模拟一千个人同时开抢。
二、遇到的问题
构造的一千个线程,同时操作数据库,可能将库存表的total字段变成负的。
在下面的实测过程中,发现了100库存最终变成了-4,这是有问题的。
其中的核心原因是:在抢单方法中,既有读数据库操作,又有写数据库操作。A线程从数据库查到的Product的库存数量是1,B线程同样是查到了是1。结果两个线程都执行了更新库存减一的操作,那么库存量就变成了 -1 了。实际情况会更糟糕。
下面是有问题的代码。
/**
* 抢单方法实现
*
* @param userId 抢单用户id
*/
@Override
public void robbingProduct(int userId) {
//先查询商品
Product product = productDao.selectProductByNo(PRODUCT_NO);
if (product != null && product.getTotal() > 0) {
//原因:多个线程可能同时进入此方法体
//再更新库存表
productDao.updateProduct(PRODUCT_NO);
//插入记录
productDao.insertProductRecord(new ProductRecord(PRODUCT_NO, userId));
//发送短信
LOGGER.info("用户{}抢单成功", userId);
} else {
LOGGER.error("用户{}抢单失败", userId);
}
}
三、解决办法
并发出现的问题,一般情况下,可以从 SQL角度、代码角度、中间件角度 来解决。
SQL优化
在更新库存的代码后面,追加 AND total > 0
追加的这个total大于0的条件非常重要。在mysql数据库中,它会让我们的t_product表的库存total字段,更新到0为止。
<update id="updateProduct">
UPDATE t_product SET total = total - 1 where productNo = #{productNo}
<!-- 下面追加的total>0非常重要-->
AND total > 0
</update>
<select id="selectProductByNo" resultType="com.safesoft.springboot.basessm.entity.Product">
SELECT * FROM `t_product` where productNo = #{productNo}
<!-- 下面追加的total>0非常重要-->
AND total > 0
</select>
代码优化
int updateResult = productDao.updateProduct(PRODUCT_NO);
在SQL优化中,如果total>0条件不成立,也就是说库存量total字段的值已经到了0。
因此,当上述代码的执行结果的返回值是0的时候,说明更新失败,数据库中total字段已经为0,商品已经被抢光。
Product product = productDao.selectProductByNo(PRODUCT_NO);
if (product != null && product.getTotal() > 0) {
//更新库存表,库存量减少1。返回1说明更新成功。返回0说明库存已经为0
int updateResult = productDao.updateProduct(PRODUCT_NO);
//如果商品没被抢光
if (updateResult > 0) {
//插入记录
productDao.insertProductRecord(new ProductRecord(PRODUCT_NO, userId));
//发送短信
LOGGER.info("用户{}抢单成功", userId);
} else {
LOGGER.error("用户{}抢单失败", userId);
}
} else {
LOGGER.error("用户{}抢单失败", userId);
}
优化后效果:
四、使用RabbitMQ进行流量削峰
最后一个问题是,如果并发量实在太大,会给我的应用程序带来非常大的压力。
首先因为频繁创建对象,对我们的堆内存造成压力。GC需要频繁销毁对象,对GC的压力也很大。
其次对数据库的压力也很大。
解决办法就是,把用户的抢单请求发送到RabbitMQ消息中间件中。因为RabbitMQ是一个消息队列,队列会按照先进先出的特点进行操作。RabbitMQ服务器一般部署在另外一个电脑上,所以就把这个并发压力转移到了另外电脑的RabbitMQ服务器上,而不是我们的抢单应用程序。
使用RabbitMQ的最主要变化就是:以前抢单操作请求直接由我们抢单应用程序执行,现在请求被转移到了RabbitMQ服务器中。RabbitMQ服务器把接收到的抢单请求进行排队,最后由RabbitMQ服务器把抢单请求转发到我们的抢单应用程序,这样的好处就是避免我们的抢单应用程序短时间直接处理大量请求。RabbitMQ服务器主要作用是减缓抢单应用程序的并发压力,相当于在我们的抢单程序之前加了一道请求缓冲区。
配置后的效果预览
RabbitMQ服务器的集成,也可参考上一节:第六节 SpringBoot集成RabbitMQ综合运用(SSM框架集成RabbitMQ)
五、源码下载
源代码地址:https://github.com/hairdryre/Study_RabbitMQ
阅读更多:从头开始学RabbimtMQ目录贴
如果本系列文章对你有帮助,不妨请我喝瓶可乐吧!
你的打赏是对我最好的支持!