我有一个电子商务网站用Spring +角编写。我需要在我的产品表中保持一个计数器,以跟踪已售出的数量。但是,当许多用户同时订购同一商品时,计数器有时会变得不准确。
在我的服务代码中,我有以下事务声明:
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
在其中,在持久化订单(使用CrudRepository.save())之后,我执行一个select查询来汇总到目前为止所订购的数量,希望select查询将计数所有已提交的订单。但情况似乎并非如此,有时候,柜台的数量比实际的要少。
同样的问题也发生在我的另一个用例:数量限制一个产品。我使用相同的事务隔离设置。在代码中,我将执行一个select查询,看看已经售出了多少,如果我们不能完成订单,就会抛出股票错误。但是对于热门项目,我们有时会超卖该项目,因为每个线程都看不到其他线程中刚刚提交的订单。
那么,对于我的用例而言,READ_COMMITTED是否是正确的隔离级别?还是我应该对这个用例做悲观的锁定?
更新05/13/17
我选择了Ruben的方法,因为我更了解java而不是数据库,所以我走了一条更容易的路。我就是这么做的。
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
public void updateOrderCounters(Purchase purchase, ACTION action)我用的是JpaRepository,所以我不直接玩entityManager。相反,我只是将代码放在单独的方法中更新计数器,并按上面的方式进行注释。到目前为止似乎效果很好。我看到超过60个并发连接发出订单,没有超卖,响应时间似乎也不错。
发布于 2017-04-29 09:39:55
根据检索总售出项目的方式,可用选项可能有所不同:
1.如果通过对订单的sum 查询动态计算已售出的商品,则对订单进行动态计数。
我相信在这种情况下,您所拥有的选项是对事务使用SERIALIZABLE隔离级别,因为这是唯一支持range locks并防止phantom reads的方法。但是,我不建议使用这个隔离级别,因为它对您的系统有很大的性能影响(或者只在设计良好的站点上非常小心地使用)。
链接:串行化
2.如果您在产品或与产品关联的其他行上维护一个计数器
在这种情况下,我可能会建议在服务方法中使用row level locking (如select for update )来检查产品的可用性,并增加销售商品的数量。产品布局的高级算法可以类似于以下步骤:
select for update查询(存储库方法上的@Lock(LockModeType.PESSIMISTIC_WRITE))检索存储剩余/售出项目数的行。id上执行select for update查询来获取锁)。您可以通过调用‘entityManager.refresh(实体)’来实现这一点。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时在结帐处理例程中)。将锁的影响最小化的另一个有用的技巧是,不是在产品本身上添加“count”列,而是在与产品关联的另一个表上添加“count”列。这将阻止您锁定产品行,因为锁将在不同的行/表组合中获得,这些组合纯粹在签出阶段使用。
链接:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
摘要
请注意,这两种技术都在系统中引入了额外的同步点,从而降低了吞吐量。因此,请确保通过性能测试或在您的项目中用于度量吞吐量的任何其他技术来仔细测量它对您的系统的影响。
很多时候,网上商店选择超卖/预订一些商品,而不是影响表现。
希望这能有所帮助。
发布于 2017-04-28 06:47:10
使用这些事务设置,您应该会看到提交的内容。但是,您的事务处理并不是很严密。可能会发生以下情况:
隔离级可序列化应该能解决这个问题。但
根据您正在使用的数据库,使用数据库约束来实现这一点可能是一个更好的主意。例如,在oracle中,您可以创建一个物化视图,计算完整的库存,并对结果设置一个非负约束。
更新
对于物化视图方法,您可以执行以下操作。
ON COMMIT子句实现的。
论承诺条款
在提交时指定,表明每当数据库提交在物化视图的主表上操作的事务时,都会发生快速刷新。此子句可能会增加完成提交所需的时间,因为数据库作为提交过程的一部分执行刷新操作。有关更多详细信息,请参阅6002.htm。
https://stackoverflow.com/questions/43672684
复制相似问题