使用哪个弹簧事务隔离级别来维护已售产品的计数器?

Posted

技术标签:

【中文标题】使用哪个弹簧事务隔离级别来维护已售产品的计数器?【英文标题】:Which spring transaction isolation level to use to maintain a counter for product sold? 【发布时间】:2017-09-26 03:04:56 【问题描述】:

我有一个使用 Spring Boot + Angular 编写的电子商务网站。我需要在我的产品表中维护一个计数器来跟踪已售出的数量。但是,当许多用户同时订购同一商品时,计数器有时会变得不准确。

在我的服务代码中,我有以下事务声明:

@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)

其中,在持久化订单后(使用CrudRepository.save()),我执行了一个选择查询来汇总到目前为止订购的数量,希望选择查询能够计算所有已提交的订单。但好像不是这样,计数器时不时会小于实际数字。

我的其他用例也出现同样的问题:产品数量限制。我使用相同的事务隔离设置。在代码中,我将执行选择查询以查看已售出多少件,如果我们无法履行订单,则抛出缺货错误。但是对于热门商品,我们有时会超卖商品,因为每个线程都看不到其他线程刚刚提交的订单。

那么READ_COMMITTED 是适合我用例的隔离级别吗?或者我应该为这个用例做悲观锁定?

2017 年 5 月 13 日更新

我选择了 Ruben 的方法,因为我对 java 的了解比对数据库的了解更多,所以我选择了更简单的方法。这就是我所做的。

@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
public void updateOrderCounters(Purchase purchase, ACTION action)

我使用的是 JpaRepository,所以我不直接玩 entityManager。相反,我只是将更新计数器的代码放在一个单独的方法中,并如上注释。到目前为止,它似乎运作良好。我已经看到超过 60 个并发连接下订单并且没有超卖,响应时间似乎也不错。

【问题讨论】:

【参考方案1】:

根据您检索已售商品总数的方式,可用选项可能会有所不同:

1.如果您通过sum 查询订单动态计算已售商品数量

我相信在这种情况下,您可以选择使用SERIALIZABLE 隔离级别进行事务处理,因为这是唯一支持range locks 并阻止phantom reads 的隔离级别。 但是,我真的不建议使用这种隔离级别,因为它会对您的系统产生重大的性能影响(或者仅在设计良好的地方非常谨慎地使用)。

链接:https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html#isolevel_serializable

2。如果您在产品或与产品相关的其他行上维护一个计数器

在这种情况下,我可能会建议在服务方法中使用row level locking,例如select for update,它会检查产品的可用性并增加已售商品的数量。产品植入的高级算法可能类似于以下步骤:

    使用select for update 查询(在存储库方法上为@Lock(LockModeType.PESSIMISTIC_WRITE))检索存储剩余/已售商品计数的行。 确保检索到的行具有最新的字段值,因为它可以从 Hibernate 会话级缓存中检索(hibernate 只会在 id 上执行 select for update 查询以获取锁)。您可以通过调用“entityManager.refresh(entity)”来实现。 检查行的count 字段,如果该值符合您的业务规则,则增加/减少它。 保存实体、刷新休眠会话并提交事务(显式或隐式)。

元代码如下:



    @Transactional
    public Product performPlacement(@Nonnull final Long id) 
        Assert.notNull(id, "Product id should not be null");
        entityManager.flush();
        final Product product = entityManager.find(Product.class, id, LockModeType.PESSIMISTIC_WRITE);
        // Make sure to get latest version from database after acquiring lock, 
        // since if a load was performed in the same hibernate session then hibernate will only acquire the lock but use fields from the cache
        entityManager.refresh(product);
        // Execute check and booking operations
        // This method call could just check if availableCount > 0
        if(product.isAvailableForPurchase()) 
            // This methods could potentially just decrement the available count, eg, --availableCount
            product.registerPurchase();
        
        // Persist the updated product 
        entityManager.persist(product);
        entityManager.flush();
        return product;
    

这种方法将确保没有任何两个线程/事务会同时对存储产品计数的同一行执行检查和更新

但是,因此它也会对您的系统产生一些性能下降的影响,因此必须确保在购买流程中尽可能使用原子增量/减量,并且尽可能少(例如,当客户点击pay 时,就在结帐处理程序中)。另一个使锁定影响最小化的有用技巧是将“计数”列添加到产品本身而不是与产品相关联的不同表上。这将阻止您锁定产品行,因为锁定将在不同的行/表组合上获得,这些组合纯粹在结帐阶段使用。

链接:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html

总结

请注意,这两种技术都会在您的系统中引入额外的同步点,从而降低吞吐量。因此,请确保通过性能测试或项目中用于测量吞吐量的任何其他技术仔细测量它对系统的影响。

网上商店通常会选择过度销售/预订某些商品,而不是影响业绩。

希望这会有所帮助。

【讨论】:

感谢详细的回复。我正在做第一种方法,将尝试第二种方法。 似乎有效。现在两天了,我还没有看到任何超卖的东西。 :)【参考方案2】:

使用这些事务设置,您应该会看到已提交的内容。但是,您的事务处理仍然不严密。可能会发生以下情况:

假设您的库存还剩一件。 现在开始了两笔交易,每笔都订购一件商品。 两者都检查库存并查看:“我的库存足够好。” 两者都提交。 现在你超卖了。

Isolation level serializable 应该可以解决这个问题。 但是

    不同数据库中可用的隔离级别差异很大,所以我认为实际上并不能保证给您请求的隔离级别

    这严重限制了可扩展性。执行此操作的事务应尽可能短且少。

根据您使用的数据库,最好使用数据库约束来实现这一点。例如,在 oracle 中,您可以创建一个计算完整库存的物化视图,并将结果约束为非负数。

更新

对于物化视图方法,您执行以下操作。

    创建物化视图,计算您想要约束的值,例如订单的总和。确保物化视图在更改底层表内容的事务中得到更新。

    对于 oracle,这是通过 ON COMMIT 子句实现的。

    ON COMMIT 子句

    指定 ON COMMIT 以指示只要数据库提交对物化视图的主表进行操作的事务,就会发生快速刷新。该子句可能会增加完成提交所需的时间,因为数据库执行刷新操作是提交过程的一部分。

    更多详情请见https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_6002.htm。

    在该物化视图上放置一个检查约束以编码您想要的约束,例如该值永远不会是负数。请注意,物化视图只是另一个表,因此您可以像往常一样创建约束。

    见例子https://www.techonthenet.com/oracle/check.php

【讨论】:

感谢您的回复。我们正在使用 MySQL 数据库。是否有使用物化视图的理由(我的数据库知识有限)。我可以在我的产品表中添加一个列作为 quantity_remain 并对其设置非负约束吗?实际上我想这并不能解决问题,因为两个线程仍然会认为还有足够的库存,并会相应地更新剩余的数量。我需要在我的数据库(触发器或其他东西)中编写代码来更新数量剩余? 我尝试过的另一种方法是在上面的父事务中启动一个新事务,只是为了对计数进行选择查询。但这似乎也不起作用(虽然我没有使用 SERIALIZE 隔离级别)。这是一种明智的做法吗? 我认为内部交易不会改变任何事情。您本质上需要的是一次影响多行的约束。这在普通约束下是不可能的,因此使用物化视图的方法。当然,另一种选择是不介意超卖和事后处理,基本上是做“最终一致性” 我明白了。您能描述一下使用物化视图解决防止超卖用例所需的步骤吗? 如果我对答案的更新就足够了。否则,我建议您为此创建一个新问题。

以上是关于使用哪个弹簧事务隔离级别来维护已售产品的计数器?的主要内容,如果未能解决你的问题,请参考以下文章

数据库事务隔离级别 一般用哪个

数据库事务及事务隔离级别

Spring 有几种事务隔离级别?

事务隔离级别

mysql -- 事务隔离级别以及java中事务提交的步骤

事务隔离级别