基于InventoryHint实现库存的热点扣减

方案说明

我们的库存扣减,是基于 Redis+数据库的,先在 Redis 中做预扣减,然后再在数据库中做扣减。

但是我们有一种方案是没用 MQ的,而是在数据库中用了InventoryHint,他可以帮我们抗高并发的热点更新。

以上是整体的交互图,下面是更加详细的系统时序图。

这里数据库在扣减的时候用了一个 InventoryHint

所谓Inventory Hint,其实就是一个补丁,是阿里云上的 RDS支持的一个功能。(官方介绍:https://help.aliyun.com/zh/rds/apsaradb-rds-for-mysql/inventory-hint )

因为我的项目中,数据库并不是自己搭建的,而是直接用的阿里云的 RDS,所以就用到了他的这个功能。

Inventory Hint的用法很简单,只需要在正常的update语句中增加上特殊的hint语句就行了,如:

<!--  库存预扣减  -->
<update id="trySale">
    UPDATE /*+ COMMIT_ON_SUCCESS ROLLBACK_ON_FAIL TARGET_AFFECT_ROW 1 */ collection
    SET saleable_inventory = saleable_inventory - #{quantity}, lock_version = lock_version + 1
    WHERE id = #{id} and <![CDATA[saleable_inventory >= #{quantity}]]>
</update>

<!--  库存退还 -->
<update id="cancelSale">
    UPDATE /*+ COMMIT_ON_SUCCESS ROLLBACK_ON_FAIL TARGET_AFFECT_ROW 1 */ collection
    SET saleable_inventory = saleable_inventory + #{quantity}, lock_version = lock_version + 1
    WHERE id = #{id} and <![CDATA[saleable_inventory  + #{quantity} < quantity]]>
</update>

<!--  库存占用  -->
<update id="confirmSale">
    UPDATE /*+ COMMIT_ON_SUCCESS ROLLBACK_ON_FAIL TARGET_AFFECT_ROW 1 */ collection
    SET occupied_inventory = occupied_inventory + #{quantity}, lock_version = lock_version + 1
    WHERE id = #{id} and <![CDATA[occupied_inventory + #{quantity} < quantity ]]>
</update>

我们的项目中,库存扣减这里采用了/*+ COMMIT_ON_SUCCESS ROLLBACK_ON_FAIL TARGET_AFFECT_ROW 1 */来标识 Hint。这里面的COMMIT_ON_SUCCESSROLLBACK_ON_FAILTARGET_AFFECT_ROW都是一些Hint语法:

  • COMMIT_ON_SUCCESS:当前语句执行成功就提交事务上下文。
  • ROLLBACK_ON_FAIL:当前语句执行失败就回滚事务上下文。
  • TARGET_AFFECT_ROW(NUMBER):如果当前语句影响行数是指定的就成功,否则语句失败。

这样,我们的库存扣减如果出现热点,就能自动检测到,然后基于 Inventory Hint 的机制做高并发的热点更新了。

原理介绍

当我们是使用COMMIT_ON_SUCCESS等hint标记了一条SQL之后,就相当于告诉MySQL内核,这行可能是热点更新。

于是,MySQL的内核层就会自动识别带此类标记的更新操作,在一定的时间间隔内,将收集到的更新操作按照主键或者唯一键进行分组,这样更新相同行的操作就会被分到同一组中。

为了进一步提升性能,在实现上,使用两个执行单元。当第一个执行单元收集完毕准备提交时,第二个执行单元立即开始收集更新操作;当第二个执行单元收集完毕准备提交时,第一个执行单元已经提交完毕并开始收集新一批的更新操作,两个单元不断切换,并行执行。

根据热点行做了分组之后,就可以作进一步优化了,这个过程主要有3个关键的优化点:

1、减少行级锁的申请等待

在同一组中,需要更新的都是同一条记录,那么根据SQL的提交顺序,就可以排队了。

然后我们只需要在第一条更新SQL(Leader)执行的时候,尝试去获取目标行的锁,如果获取成功,则开始操作。

然后这一组中后续的更新操作(Follower)也会尝试获取锁,但是会先判断是不是已经被第一条更新操作获取到了,如果是的话,那么就不需要等待,直接获取锁。

这样就可以大大降低行级锁的申请的阻塞等待时长。

2、减少B+树的索引遍历操作

MySQL是以B+索引的方式管理数据的,每次执行查询时,都需要遍历索引才能定位到目标数据行,数据表越大,索引层级越多,遍历时间就越长。

如果针对热点行更新操作做了分组之后,我们只需要在每组的第一条SQL执行过程中,通过遍历索引定位数据行,之后就可以把这些数据行缓存到Row Cache中,并且在Row Cache进行修改。

在同组的后续操作时,也不再需要进行数据索引了,直接从Row Cache获取数据并修改就行了。

这样就大大降低了B+树的索引遍历操作的耗时。

3、减少事务提交次数

如果是没有用这种方式,我们的多条update语句会是多条事务,那么每一个事务都要单独做一次提交。

有了分组、排队、组提交之后,就只需要一组中的并发操作都执行完,然后做一次组提交即可,大大降低提交次数。

### Java 实现订单库存扣减时的并发控制与加锁机制最佳实践 #### 1. 使用 Redis 进行库存扣减 为了确保高并发环境下的库存准确性,可以使用 Redis 来预先扣除库存。由于 Redis 是单线程模型并具备原子操作特性,因此非常适合用来处理这种场景。 ```java public class InventoryService { private final Jedis jedis; public boolean preDeductInventory(String productId, int quantity) { Long result = jedis.decrBy(productId, quantity); return result >= 0; } } ``` 当商品ID对应的键值小于零时表示库存不足[^1]。 #### 2. 数据库层面使用乐观锁 通过版本号字段 `version` 或者时间戳字段 `update_time` 对记录进行更新时增加条件判断,只有满足特定条件下才允许修改数据,从而避免脏读和幻读现象的发生。 ```sql UPDATE inventory SET stock=stock-?, version=version+1 WHERE product_id=? AND version=? ``` 对应到Java代码中: ```java @Transactional public void deductStockWithOptimisticLock(Long id, Integer count){ String sql = "UPDATE t_goods SET num=num-? , version=version+1 where id=? and version=?"; // 执行SQL语句... } ``` 如果影响行数为0,则说明当前存在其他事务正在对该条目做更改,此时应该抛出异常或者重试业务逻辑[^2]。 #### 3. 分布式锁控制并发访问 对于跨服务之间的竞争资源情况,可借助于像 Zookeeper、Redisson 提供的分布式互斥锁功能来协调多个实例间的同步问题。 利用 Redission 客户端创建公平锁 FairLock: ```java import org.redisson.api.RLock; import org.redisson.api.RedissonClient; @Autowired private RedissonClient redisson; public void processOrder(Order order) throws InterruptedException{ RLock lock = redisson.getFairLock("order:" + orderId); try { if (lock.tryLock(10, TimeUnit.SECONDS)) { // 处理下单流程... } else { throw new RuntimeException("Failed to acquire distributed lock"); } } finally { lock.unlock(); } } ``` 此方法能够有效防止因多节点同时请求而导致的数据混乱状况发生。 #### 4. 利用消息队列降低压力 引入 MQ 中间件作为缓冲区,将瞬时高峰流量平滑化分布至较长时间内逐步消化吸收掉,进而减轻后端服务器负载强度以及减少超卖风险。 生产者发送消息给 RabbitMQ 队列之后立即返回成功响应给前端页面展示已购买提示信息;消费者监听指定主题接收到来自生产者的指令后再去执行实际的商品数量变更动作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

埃泽漫笔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值