MySQL作为常用的关系型数据库,在高并发业务场景中,多个线程或进程同时对同一条数据进行写入操作时,很容易出现写入冲突,比如经典的更新丢失问题,也就是后提交的操作覆盖了先提交操作的修改结果。要避免这类问题,需要结合MySQL的并发写控制机制设计合理的方案。

基于事务隔离级别避免写入冲突
MySQL的事务隔离级别直接影响并发操作时的数据可见性和冲突情况,不同的隔离级别对写入冲突的规避能力不同。默认的REPEATABLE READ级别已经能解决大部分幻读和不可重复读问题,若业务对数据一致性要求更高,可以调整到SERIALIZABLE级别,但会牺牲一定的并发性能。
可以通过如下语句查看和设置当前会话的事务隔离级别:
-- 查看当前事务隔离级别 SELECT @@transaction_isolation; -- 设置当前会话的事务隔离级别为可重复读 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
使用锁机制控制并发写入
悲观锁
悲观锁假设冲突一定会发生,在操作数据前先加锁,其他事务需要等待锁释放才能操作对应数据。MySQL的FOR UPDATE语句可以在查询时加排他锁,适合写操作频繁、冲突概率高的场景。
示例:查询用户余额并加锁,避免其他事务同时修改该用户的余额:
-- 开启事务 START TRANSACTION; -- 查询用户id为1的余额并加排他锁 SELECT balance FROM user_account WHERE user_id = 1 FOR UPDATE; -- 执行更新操作,比如扣减100元 UPDATE user_account SET balance = balance - 100 WHERE user_id = 1; -- 提交事务,释放锁 COMMIT;
乐观锁
乐观锁假设冲突不会频繁发生,不在操作前加锁,而是在更新时判断数据是否被其他事务修改过。常用的实现方式是在表中增加版本号字段,更新时携带版本号条件,若版本号匹配才执行更新,同时版本号自增。
表结构示例:
CREATE TABLE user_account (
user_id INT PRIMARY KEY,
balance DECIMAL(10,2),
version INT DEFAULT 0
);
乐观锁更新逻辑示例:
-- 先查询当前数据及版本号 SELECT balance, version FROM user_account WHERE user_id = 1; -- 假设查询到的version是5,执行更新时携带版本条件 UPDATE user_account SET balance = balance - 100, version = version + 1 WHERE user_id = 1 AND version = 5; -- 如果受影响行数为1,说明更新成功;如果为0,说明版本号不匹配,数据被其他事务修改过,需要重试
分布式场景下的写入冲突避免
如果是分布式系统,多个服务实例同时操作MySQL,仅靠数据库本身的锁可能不够,可以结合分布式锁,比如基于Redis实现的分布式锁,在业务层先获取锁再执行数据库写入操作,避免跨实例的写入冲突。
简单的Redis分布式锁伪代码示例:
// 尝试获取分布式锁,锁的key为user_update_1,过期时间3秒
String lockKey = "user_update_1";
String requestId = UUID.randomUUID().toString();
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 3, TimeUnit.SECONDS);
if (lockSuccess) {
try {
// 执行MySQL写入操作
updateUserBalance(1, -100);
} finally {
// 释放锁,需要校验requestId避免误删其他请求的锁
String currentRequestId = redisTemplate.opsForValue().get(lockKey);
if (requestId.equals(currentRequestId)) {
redisTemplate.delete(lockKey);
}
}
} else {
// 获取锁失败,重试或者返回操作繁忙提示
}
不同方案的适用场景对比
| 方案 | 适用场景 | 优缺点 |
|---|---|---|
| 事务隔离级别调整 | 对数据一致性要求高,冲突概率中等的场景 | 实现简单,无需额外代码,但高隔离级别会影响并发性能 |
| 悲观锁 | 写操作频繁,冲突概率高的场景 | 能严格避免冲突,但加锁会增加等待时间,可能出现死锁 |
| 乐观锁 | 读多写少,冲突概率低的场景 | 无锁,并发性能好,但冲突时需要重试,增加业务复杂度 |
| 分布式锁 | 分布式多实例部署的场景 | 能跨实例控制冲突,但需要额外引入中间件,实现复杂度高 |
实际业务中需要根据具体的并发量、冲突概率、数据一致性要求选择合适的方案,也可以组合使用多种方案,比如在分布式场景下同时使用乐观锁和分布式锁,进一步降低写入冲突的概率。