我正在为Symfony应用程序编写一个功能,该功能允许用户提交产品评级。我在每次评级后计算产品评级的平均值,这样我就不需要每次需要平均评级时都运行可能昂贵的AVG()
查询。
这里有一个简单的函数,可以计算平均评级并保存:
public function calculateAndSaveAverageRating(Product $product)
{
// Run the SUM() query and return a float containing the average,
// or null if there are no ratings for the product.
$calculatedAverage = $this
->em
->getRepository('AppBundle:Product')
->findAverageRating($product);
// Lookup an existing ProductRatingAverage entity if it exists. This
// stores the average value of ratings for each product. Returns null
// if there is no existing entity.
$existingAverageEntity = $this
->em
->getRepository('AppBundle:ProductRatingAverage')
->findOneBy(array('product' => $product));
// Save the calculated average if we got a non-null value. Otherwise
// there are no ratings for this product, so delete the existing
// average entity if it exists.
if ($calculatedAverage) {
// If we have an existing average entity, update it. Otherwise
// create a new one and store the average.
if ($existingAverageEntity) {
$existingAverageEntity->setAverage($calculatedAverage);
} else {
$existingAverageEntity = new ProductRatingAverage();
$existingAverageEntity->setProduct($product);
$existingAverageEntity->setAverage($calculatedAverage);
$this->em->persist($existingAverageEntity);
}
} else {
if ($existingAverageEntity) {
$this->em->remove($existingAverageEntity);
}
}
$this->em->flush();
}
但这里也有一些并发问题。这里有两个:
- 如果两个用户同时(或非常接近)提交先前没有评级的产品的评级,则此代码将尝试为同一产品创建两个平均评级实体,而只能有一个(数据库唯一约束)
- 如果两个用户同时(或非常接近)提交了先前评级的产品的评级,则此代码可能会从计算的平均值中排除其中一个评级
我可以采取不同的方法:在运行AVG()
查询的查询前面放置一个即将过期的缓存,并使其每1小时或更长时间过期一次。然而,我遇到了同样的问题:如果两个访问者同时触发缓存刷新,就会出现同样的并发问题。
我应该如何设计此代码以最大限度地减少并发问题?
并发的解决方案通常是锁定机制。有时只需要一行锁定,而其他时候则锁定整个表。第一个事务应用表锁定,此后用于搜索或修改数据的其余事务保持等待,直到第一个事务明确地执行解锁。我建议你阅读以下链接http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html#locking-支持。