管理数据库中的产品计数

时间:2016-02-17 18:41:12

标签: java database multithreading concurrency thread-safety

对不起,如果这个问题看起来很幼稚,但我遇到过需要管理电子商务商店数据库中的产品数量的情况。

有一个带有整数变量productCount的Product类,它表示数据库中可供网站用户看到的可用产品数。现在这个类可以被多个线程访问,或者可以说电子商务站点的几个用户。每个人都在添加或删除产品到他的购物车。

正在使用的ORM框架是hibernate

示例代码

@Entity
@Table
class Product{
   @Column
   private int productCount;

   public void addProductToCart(){
     // decrements the product count by 1 & updates the database
   }

   public void removeTheProductFromTheCart(){
    // increments the product count by 1 & updates the database
   }

从代码中可以清楚地看到,我需要对数据库中的产品计数进行并发检查,以防止丢失更新。

此外,如果有多个用户尝试在数据库中仅添加单个左侧产品。应该将产品添加到哪个用户的购物车中?

我对此进行了一些研究

我找到的可能方法是

  1. 为Product创建单例类。这将确保整个应用程序中只有一个产品实例可用。

  2. 同步addProductToCart& removeTheProductFromTheCart方法。 这将只允许一个线程更新产品数量&一次更新数据库。

  3. 使用数据库并发控制为productCount应用一些db事务隔离级别,乐观/悲观锁定。我使用的是mysql,默认隔离级别为REPEATABLE_READ

  4. 处理这个问题的最佳方法是什么?

9 个答案:

答案 0 :(得分:7)

第3。使用数据库并发控制

为什么?

  • 1&如果您的电子商务应用绝对是修改产品数量的唯一方法,那么2可以。这是一个很大的问题。在开展业务和维护库存的过程中,商店可能需要其他方式来更新产品数量,电子商务应用程序可能不是理想的解决方案。另一方面,数据库通常更容易挂钩到有助于商店库存过程的不同应用程序。

  • 数据库产品通常具有许多故障安全机制,因此如果出现问题,您可以跟踪哪些事务成功,哪些事务没有,并且您可以回滚到特定时间点。漂浮在内存中的java程序没有开箱即用的功能,你必须自己开发它,如果你做了1或2. Spring和Hibernate以及其他类似东西肯定比什么都好,只比较它们提供什么和什么数据库提供了从某些电子灾难中恢复的能力。

答案 1 :(得分:3)

对于您正在考虑的前两种可能性,仅当您仅限于部署应用程序的单个实例时,这些工作才有效。您不能在多个应用程序实例之间管理单例,也不能跨多个JVM进行同步。因此,如果您使用其中一个部署选项将受到约束,那么部署应用程序的多个实例的唯一方法是,如果您执行的操作将会话固定到特定实例,这对于负载平衡是不利的。所以这些看起来都不合适。

从数据库获取产品计数的方法具有以下优点:当应用程序在多个实例中扩展而不会破坏负载平衡时,它仍然有效。

你可能会想,这只会是一台服务器上的一个实例,所以我可以顺便说一句。但是在您构建应用程序时,可能并不完全清楚应用程序的部署方式(我一直处于这样的情况:在应用程序在preprod环境中设置之前我们不知道计划是什么)或者在以后可能有理由改变应用程序的部署方式;如果您的应用程序具有超出预期的负载,那么设置第二个框可能是有益的。

对我来说,有一点不明显的是,产品数量实际上是多么重要。在不同的业务领域(机票,航运),过度使用是常见的,并且保持100%准确计数可能比值得更麻烦,特别是如果它在过程的早期阶段,例如在购物中添加商品购物车(与客户实际承诺进行购买的点相比)。在客户购买物品时,确保您使用数据库交易保留这些物品可能更有意义(或者不是,再次参考超量预订)。

在Web应用程序中,预计从购物车中的商品到实际购买的商品的转化率很低,这似乎很常见。请注意,您的计数的准确程度适合您的业务领域。

答案 2 :(得分:2)

IMO传统的分层方法在这方面会有所帮助 - 不确定这会有多么激进,因为我们不知道应用程序的大小/成熟度,但是无论如何都要继续描述它,你可以选择哪些是可行的。

理论......

Services   a.k.a. "business logic", "business rules", "domain logic" etc.
 ^
DAOs       a.k.a. "Data Access Objects", "Data Access Layer", "repository" etc.
 ^
Entities   a.k.a. "model" - the ORM representation of the database structure
 ^
Database

将实体与DAO层分开是有用的,因此它们只是简单的存储单元,您可以填充,比较等,而不包括对其起作用的方法。所以这些只是数据库中 的一个类表示,理想情况下不应该使用定义 将如何使用它们的代码进行污染。

DAO层提供基本的CRUD操作,允许持久化,检索,合并和删除这些实体,而无需知道完成此操作的上下文。这是一个单例可以用来防止一次又一次地创建多个实例的地方 - 但使用单例并不意味着线程安全。我个人建议使用Spring来做这个(默认情况下,Spring bean是单例),但如果愿意,可以手动完成。

服务层是"域逻辑"实现,即应用程序执行特定功能所需的特定操作组合。线程安全问题可以在这里得到解决,有时需要它和不需要的时间。

在实践中......

按照这种方法,您可能会得到类似的结果(为简洁起见省略了许多内容):

@Entity
@Table
public class Product {
    @ManyToOne
    @JoinColumn
    private ShoppingCart shoppingCart;
}

@Entity
@Table
public class ShoppingCart {
    @OneToOne
    @JoinColumn
    private User user;

    @OneToMany(mappedBy = "shoppingCart")
    private Set<Product> products;
}

public class ShoppingCartDao { /* persist, merge, remove, findById etc. */ }

@Transactional
public class ProductService() {
    private ConcurrentMap<Integer, Integer> locks = 
        new ConcurrentHashMap<Integer, Integer>();

    public void addProductToCart(final int productId, final int userId) {
        ShoppingCart shoppingCart = shoppingCartDao.findByUserId(userId);                               
        Product product = productDao.findById(productId);
        synchronized(getCacheSyncObject(productId)) {
            if (product.shoppingCart == null) {
                product.setShoppingCart(shoppingCart);
            } else {
                throw new CustomException("Product already reserved.");
            }
        }
    }

    public void removeProductFromCart(final int productId, final int userId) {
        ShoppingCart shoppingCart = shoppingCartDao.findByUserId(userId);
        Product product = productDao.findById(productId);
        if (product.getShoppingCart() != shoppingCart) {
            throw new CustomException("Product not in specified user's cart.");
        } else {
            product.setShoppingCart(null);
        }
    }

    /** @See http://stackoverflow.com/questions/659915#659939 */
    private Object getCacheSyncObject(final Integer id) {
      locks.putIfAbsent(id, id);
      return locks.get(id);
    }       
}

答案 3 :(得分:2)

正确的方法是使用数据库锁,因为它是为这项工作而设计的。如果你正在使用hibernate,那么使用LockRequest非常简单:

Session session = sessionFactory.openSession()
Transaction transaction;
boolean productTaken = false;

try {
    transaction = session.beginTransaction();
    Product product = session.get(Product.class, id);
    if (product == null)
        throw ...

    Session.LockRequest lockRequest = session.buildLockRequest(LockOptions.UPGRADE);
    lockRequest.lock(product);
    productTaken = product.take();
    if (productTaken) {
        session.update(product);
        transaction.commit();
    }
} finally {
    if (transaction != null && transaction.isActive())
        transaction.rollback();
    session.close();
}

这里我们从数据库中提取产品进行更新,以防止任何并发更新。

答案 4 :(得分:1)

让我们评估三个选项。

  

1.为Product创建一个单例类。这将确保整个应用程序中只有一个产品实例可用。

产品的单一实例很好。但是,如果您提供数量为20的移动设备产品,则increment上的addProductToCart产品数量(静态变量)和decrement上的removeTheProductFromTheCart产品数量仍需{}}。您仍然必须同步对此可变计数的访问或更新数据库并读取产品计数。

  

2.同步addProductToCart&amp; removeTheProductFromTheCart方法。这将只允许一个线程更新产品数量&amp;一次更新数据库。

这是一个解决方案,但我更喜欢第三个:在应用程序中删除同步并在数据库层提供数据一致性。

  

3.使用数据库并发控制为productCount应用一些db事务隔离级别,乐观/悲观锁定。我使用的是mysql,默认的隔离级别是REPEATABLE_READ。

将数据库的一致性推迟到应用程序。但您必须使用READ COMMITTED作为隔离级别而不是REPEATABLE_READ

看看这个article

  

READ COMMITTED

A somewhat Oracle-like isolation level with respect to consistent (nonlocking) reads:即使在同一事务中,每次一致性读取也会设置并读取自己的新快照。

答案 5 :(得分:1)

据我所知,推车也在db中持续存在?并且最终结果也购买了产品。

产品:

[{id:1, count:100}]

购物车:

[{user_id:1, product_id:1}, {user_id:2, product_id:1}]

买了:

[{user_id:3, product_id:1}]

然后你可以计算产品

select count as total from product where id = 1
select count(*) as in_cart from cart where product_id = 1
select count(*) as total_bought from bought where product_id = 1

现在您可以通过

显示最终计数
int count = total - (in_cart + total_bought);

这样您就可以保证不会覆盖或绕过增量/减量。最后,除了代码检查计数之外,您还可以在数据库级别添加一个触发器,用于检查总计数以及产品是否可以插入购物车或购买的表格中。

如果您的产品数量每天都在变化,我的意思是昨天您有100件产品并且已售出20件,今天已经有50件产品到货,因此您应该有150 - 20件销售量,即130件。为了获得良好的报告,您可以每天计算产品数量。像

 [
  {id:1, count:100, newly_arrived: 0, date: 23/02/2016}, 
  {id:1, count:80, newly_arrived: 50, date: 24/02/2016}
 ]

然后您的查询将更改为

select count+newly_arrived as total from product where id = 1 and date = today
select count(*) as in_cart from cart where product_id = 1 and date = today
select count(*) as total_bought from bought where product_id = 1 and date = today

为此,您必须仅在午夜00:00:00插入新产品数据,当您的新产品在早上到达时,您可以更新new_arrived而无需干预购物车或购买的桌子上的任何插入或删除操作。并且您可以轻松获得详细的报告,而无需进行复杂的报告查询:))

答案 6 :(得分:1)

如果必须为此使用关系数据库(而不是密钥值存储,例如)。我强烈建议尽可能靠近存储,以避免锁定和冲突,并尽可能获得最高性能。

另一方面,这听起来像一个典型的情况,多个节点正在一个数据库上工作,这也会因Hibernate会话处理而产生延迟问题。

最后你需要像

这样的陈述
UPDATE product SET productCount = productCount + 1 WHERE id = ?

要在事务中执行,可以使用简单的JDBC语句,也可以使用带有@Query注释的JPA存储库方法。

要让Hibernate知道更改,您可以在执行此类操作后在产品上使用Session.refresh()

答案 7 :(得分:1)

使用消息队列在电子商务网络应用中完成这项工作的最佳方式。您可以使用 RabbitMQ

  1. 在rabbitMQ上创建一个队列并保留所有减少产品数量的请求

  2. 因为队列所有请求都会逐个处理,所以不会有任何冲突来减少产品。

答案 8 :(得分:-1)

这不是一个技术问题,而是一个过程问题。就个人而言,在有人实际购买产品之前,我不会减少库存。将商品添加到购物车是他们可以购买它的指标。不是他们有。

为什么不在付款成功处理后才减少库存盘点?将整个事物包装在数据库事务中。如果10个人将相同的项目添加到他们的购物车并且只剩下1个,则第一个支付获胜。 9个人会感到不安,他们没有付出更快的代价。

航空公司的参考不同。他们确实发布临时保留,但它比管理库存更复杂。他们正在使用一个流动的座位池,当库存变得稀缺时,它们会随之而来。