我有一个带有Spring Boot Angular的电子商务网站。我需要在我的产品表中维护一个柜台,以跟踪已售出的柜台。但是,当许多用户同时订购同一项目时,计数器有时会变得不准确。
在我的服务代码中,我有以下交易声明:
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
在持续订单后(使用CrudRepository.save()
(,我进行选择查询以总和到目前为止的订购数量,希望SELECT查询能计算所有订单所有订单。但这似乎并非不时,计数器小于实际数字。
我的其他用例也发生了同一问题:数量限制了产品。我使用相同的交易隔离设置。在代码中,我将进行选择查询,以查看如果我们无法履行订单,则已经出售了多少股票。但是对于热门物品,我们有时会超越该项目,因为每个线程都不看到其他线程中的订单。
那么READ_COMMITTED
是我用例的正确隔离级别吗?或者我应该为此用例做悲观的锁定?
更新05/13/17
我选择了Ruben的方法,因为我对Java更了解Java,而不是数据库,因此我走了更容易的道路。这是我所做的。
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
public void updateOrderCounters(Purchase purchase, ACTION action)
我使用的是jparepository,所以我不直接播放EntityManager。取而代之的是,我只是将代码以单独的方法更新计数器,并如上所述。到目前为止,它似乎运行良好。我已经看到> 60个并发连接订单,没有超售订单,响应时间似乎也可以。
取决于您如何检索总售出的项目计数计数可用选项可能不同:
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
EG select for update
,该方法检查产品的可用性并增加了已售出的项目的数量。产品放置的高级算法可能类似于以下步骤:
- 使用
select for update
查询(在存储库方法上的@Lock(LockModeType.PESSIMISTIC_WRITE)
(检索存储剩余项目数量的行。 - 确保检索到的行具有最新的字段值,因为可以从Hibernate Session Level Cache检索(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
摘要
请注意,这两种技术都会在系统中引入额外的同步点,从而减少吞吐量。因此,请确保通过性能测试或项目中用于测量吞吐量的任何其他技术仔细测量其对系统的影响。
经常在线商店选择过度销售/预订一些物品,而不是影响性能。
希望这会有所帮助。
使用这些事务设置,您应该看到所承诺的内容。但是,您的交易处理并不紧密。以下可能发生:
- 假设您还有一件物品。
- 现在开始了两次交易,每个交易订购一个项目。
- 既检查库存,又可以看到:"对我来说有足够的库存。"
- 两个提交。
- 现在你超卖了。
隔离水平可序列化应解决。但
-
不同数据库中可用的隔离级别差异很大,因此我认为它实际上不能保证为您提供所需的隔离级别
-
这严重限制了可扩展性。这样做的交易应尽可能短而且很少。
根据您使用的数据库,使用数据库约束实现此功能可能是一个更好的主意。例如,在Oracle中,您可以创建一个实现的视图来计算完整库存,并对结果限制为非负值。
update
对于实现的视图方法,您要执行以下操作。
-
创建实现的视图,以计算要约束的值,例如订单之和。确保在更改基础表的内容的交易中进行了更新。
对于Oracle,这是由
ON COMMIT
子句实现的。在提交条款上
指定提交以表明每当数据库在实体视图的主表上运行的事务时,都会发生快速刷新。该条款可能会增加完成提交所花费的时间,因为数据库作为提交过程的一部分执行刷新操作。
请参阅https://docs.oracle.com/cd/b19306_01/server.102/b14200/statements_6002.htm有关更多详细信息。
-
对该实现的视图进行检查约束,以编码所需的约束,例如价值永远不会负。请注意,实现的视图只是另一个表,因此您可以像正常的那样创建约束。
请参阅前示例https://www.techonthenet.com/oracle/check.php