问题: 这段代码会不会有问题
@Override
public void increaseSpecialPrizeNum(String city) {
LambdaQueryWrapper<City> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(City::getCityName, city);
City cityDb = this.getOne(queryWrapper);
if (cityDb == null) {
throw new ServiceException("城市不存在");
}
cityDb.setWinningQuotaFinish(cityDb.getWinningQuotaFinish() + 1);
this.updateById(cityDb);
}
回答: 会存在问题
提供的代码片段确实可能在高并发情况下遇到问题,尤其是在更新城市的已中奖数量(winningQuotaFinish
)时。问题的核心在于多个并发请求可能同时读取同一城市的当前已中奖数量,并基于这个值进行增加操作,而这可能导致一种称为“丢失更新”的并发问题。
解决这类问题的常见方法包括:
1. 使用乐观锁
乐观锁是一种常用的并发控制方法,特别适合读多写少的场景。您可以在 City
实体中添加一个版本号字段(例如 version
),并在每次更新时自动增加这个版本号。更新操作将基于版本号,确保在更新期间数据未被其他事务更改。示例代码如下:
public void updateCity(String city) {
LambdaQueryWrapper<City> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(City::getCityName, city);
City cityDb = this.getOne(queryWrapper);
if (cityDb == null) {
throw new ServiceException("城市不存在");
}
int currentVersion = cityDb.getVersion();
cityDb.setWinningQuotaFinish(cityDb.getWinningQuotaFinish() + 1);
LambdaUpdateWrapper<City> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(City::getId, cityDb.getId())
.eq(City::getVersion, currentVersion)
.set(City::getWinningQuotaFinish, cityDb.getWinningQuotaFinish())
.set(City::getVersion, currentVersion + 1);
boolean updated = this.update(updateWrapper);
if (!updated) {
throw new ServiceException("更新失败,请重试");
}
}
2. 使用悲观锁
悲观锁适用于写操作频繁的场景。您可以在事务中锁定特定的行,直到事务完成。这通常通过选择具有锁定行为的查询来实现。示例代码如下:
@Transactional
public void updateCity(String city) {
City cityDb = ...
if (cityDb == null) {
throw new ServiceException("城市不存在");
}
cityDb.setWinningQuotaFinish(cityDb.getWinningQuotaFinish() + 1);
this.updateById(cityDb);
}
在使用 SELECT ... FOR UPDATE
时,请确保您的事务管理设置正确,并注意这种方法可能导致数据库性能下降。
3. 在数据库层面处理
如果使用如 MySQL 这样的关系数据库,您可以考虑在数据库层面使用原子更新操作。例如:
UPDATE city SET winning_quota_finish = winning_quota_finish + 1 WHERE city_name = ?
这种方法避免了应用层面的并发问题,因为数据库会处理原子性。
选择合适的方法
选择哪种方法取决于您的应用特性和环境。乐观锁适合读多写少的场景,对性能影响较小,但可能需要处理更新冲突。悲观锁适用于更新频繁的场景,但可能导致数据库性能问题。数据库层面的原子更新最简单直接,但可能不够灵活。
额外疑问: 方法3为什么不会出现问题, 我感觉方法3和我的那个方法效果是一样的呀
回答:
从某种程度上来说,方法3(在数据库层面直接执行原子更新操作)和您的原始方法(先读取数据,然后在应用层面上增加值,最后再更新数据库)在逻辑上是相似的。然而,关键的区别在于它们处理并发更新时的行为。
方法3(数据库层面的原子更新操作)
当您直接在数据库层面执行一个原子更新操作,如:
UPDATE city SET winning_quota_finish = winning_quota_finish + 1 WHERE city_name = ?
这个操作是原子性的,这意味着数据库会自动处理并发问题。即使多个请求同时尝试更新同一行,数据库系统会确保每次更新都是在最新的数据上执行的,不会有任何中间步骤导致数据不一致。数据库内部的锁机制会确保在一个时间点上只有一个操作能够修改数据。
您的原始方法(应用层面的读-修改-写操作)
在您的原始方法中,更新操作分为三个步骤:
-
读取数据:应用从数据库中读取当前的
winning_quota_finish
值。
-
修改数据:应用在内存中将
winning_quota_finish
的值增加。
-
写回数据:应用将新的
winning_quota_finish
值写回数据库。
在高并发的场景下,这个过程可能导致所谓的“读-改-写”竞态条件。假设两个并发请求几乎同时读取了相同的 winning_quota_finish
值,它们都会在内存中对这个值进行增加,然后尝试将其写回数据库。这可能导致实际的 winning_quota_finish
值只增加了一次,而不是两次,因为两个请求都是基于最初读取的相同值进行操作的。
总结来说,方法3(数据库层面的原子更新)能更有效地处理并发情况,因为数据库系统内部会管理必要的锁,以确保每次更新都基于最新的数据,从而避免了并发更新导致的问题。而应用层面的读-修改-写操作在高并发下则可能导致数据不一致,除非采取额外的措施(如乐观锁或悲观锁)来控制并发访问。