数据库并发冲突及解决方案
大约 10 分钟
数据库并发冲突及解决方案
在高并发场景下,多个线程对同一个账户进行并发更新时,可能会产生数据不一致的问题。具体场景如下:
- 两个线程获取同一个账户的余额。
- 线程一和线程二分别在账户上增加金额。
- 线程一和线程二分别将增加后的金额更新回数据库。
如果两个线程并发执行上述操作,会导致数据不一致。例如:
- 线程一获取账户余额为400。
- 线程二在线程一更新前,获取账户余额并增加金额至500。
- 线程一更新账户余额为900(错误结果),而预期结果应为1000。
传统解决方案:悲观锁
悲观锁的实现
在原有的 SELECT
语句上增加 FOR UPDATE
子句,以锁定查询到的数据行。例如:
SELECT * FROM ACC WHERE id = 1001 FOR UPDATE;
悲观锁的执行流程
- 线程一执行
SELECT ... FOR UPDATE
,获取并锁定账户数据。 - 线程二尝试获取同一账户数据,但因被锁定处于阻塞等待状态。
- 线程一更新账户数据并提交事务,释放锁。
- 线程二获取账户数据并进行更新。
悲观锁的缺点
- 并发性差:高并发场景下,任何对同一账户的操作都会阻塞等待,降低系统并发性能。
- 用户体验差:用户操作时可能长时间等待,影响用户体验。
- 线程阻塞问题:大量线程阻塞可能导致系统线程池爆满,轻则系统失去响应,重则系统崩溃。
现代解决方案:乐观锁
乐观锁的实现
通过在数据表中增加版本号字段(如 version
),并在更新时进行版本号检查,以控制并发。例如:
ALTER TABLE ACC ADD COLUMN version INT DEFAULT 1;
乐观锁的执行流程
获取账户数据及版本号:
SELECT * FROM ACC WHERE id = 1001;
增加账户余额,并在更新时检查版本号:
UPDATE ACC SET balance = new_balance, version = version + 1 WHERE id = 1001 AND version = old_version;
如果
UPDATE
语句影响的行数为0,说明版本号不匹配,更新失败。 这个时候可以可以由系统或者用户发起重试。
乐观锁的优点
- 提高并发性:避免线程阻塞,提高系统并发性能。
- 数据一致性:通过版本号检查,确保数据更新的正确性。
乐观锁的缺点
- 用户体验问题:如果并发冲突频繁,用户可能需要多次重试操作。
具体实现
数据表结构
CREATE TABLE ACC (
id INT PRIMARY KEY,
balance DECIMAL(10, 2),
version INT DEFAULT 1
);
伪代码示例
// 获取账户数据及版本号
Account acc = select * from ACC where id = 1001;
int oldVersion = acc.getVersion();
acc.setBalance(acc.getBalance() + 100);
// 更新账户数据及版本号
int count = update ACC set balance = newBalance, version = version + 1
where id = 1001 and version = oldVersion;
if (count == 0) {
throw new VersionException("并发冲突,更新失败");
}
处理并发冲突
- 前端提示用户重试:当发生并发冲突时,提示用户重新提交请求。
- 后端自动重试:使用
Spring Retry
组件在后台自动重试操作。
Spring Retry 组件使用
Maven依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>x.x.x</version>
</dependency>
方法重试示例
总结
在高并发场景下,使用乐观锁可以有效避免数据不一致问题,同时提高系统并发性能和用户体验。
通过版本号控制并发更新,并结合前端提示或后端自动重试机制,可以确保数据的正确性和系统的健壮性。