You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot/JPA+MySQL异步Upsert操作如何避免死锁?

嘿,针对你遇到的Spring Boot/JPA + MySQL upsert死锁问题,结合你提到的异步并发调用、大量请求涌入的场景,我整理了几个实用的解决思路,你可以一步步排查优化:

一、先搞懂死锁的核心诱因

你这种场景下的死锁,大概率是因为并发upsert时不同事务获取行锁的顺序不一致,再加上单个请求重复触发upsert、事务持有锁时间过长,多重因素叠加导致的。比如两个事务分别先锁行A再锁行B,另一个反向操作,就很容易形成交叉等待的死锁。

二、具体优化措施

1. 统一行锁获取顺序(最关键)

不管是单个请求的多次调用,还是不同请求的并发操作,都要保证操作同一批数据时,按固定顺序锁定行。比如按主键ID的升序处理,这样所有事务都会先锁ID小的行,再锁ID大的,从根源上避免交叉等待锁的情况。

举个批量upsert的例子:

// 先对要操作的用户ID排序
List<Long> sortedUserIds = userIds.stream().sorted().collect(Collectors.toList());
for (Long userId : sortedUserIds) {
    // 执行upsert逻辑
    userScoreRepository.upsertScore(userId, addScore);
}

2. 用MySQL原生原子upsert替代JPA默认逻辑

Spring JPA的save()方法本质是先查询再插入/更新,中间存在时间间隙,容易引发锁冲突。直接用MySQL原生的INSERT ... ON DUPLICATE KEY UPDATE语句,原子性更强,能大幅减少锁竞争的窗口。

你可以在Repository层定义原生SQL查询:

@Repository
public interface UserScoreRepository extends JpaRepository<UserScore, Long> {
    @Modifying
    @Query(value = "INSERT INTO user_score(user_id, score, update_time) " +
                   "VALUES (:userId, :score, NOW()) " +
                   "ON DUPLICATE KEY UPDATE score = score + :score, update_time = NOW()",
           nativeQuery = true)
    void upsertScore(@Param("userId") Long userId, @Param("score") Integer score);
}

3. 控制单个请求的重复调用(幂等性)

单个请求并发多次调用upsert完全是冗余操作,还会加剧锁冲突。你可以在服务层加幂等控制,比如用Redis分布式锁(分布式场景)或本地锁(单实例场景),同一个请求ID只允许执行一次upsert。

示例(Redis幂等实现):

@Service
public class UserScoreService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private UserScoreRepository userScoreRepository;

    public void upsertUserScore(String requestId, Long userId, Integer score) {
        // 用请求ID作为锁键,设置过期时间防止死锁
        Boolean lockAcquired = redisTemplate.opsForValue()
                .setIfAbsent(requestId, "locked", 5, TimeUnit.MINUTES);
        
        if (lockAcquired != null && lockAcquired) {
            try {
                userScoreRepository.upsertScore(userId, score);
            } finally {
                redisTemplate.delete(requestId);
            }
        } else {
            log.info("请求{}已处理过用户{}的upsert操作,跳过重复执行", requestId, userId);
        }
    }
}

4. 缩短事务持有时间

尽量让事务只包裹必要的数据库操作,避免在事务里做远程调用、复杂计算等耗时操作。如果服务层方法里有大量非DB逻辑,建议把DB操作抽出来单独加事务:

@Service
public class UserScoreService {
    @Autowired
    private UserScoreRepository userScoreRepository;

    // 非事务方法:处理业务校验、日志等逻辑
    public void handleScoreUpdate(Long userId, Integer score) {
        doParameterCheck(userId, score);
        doLogRecord(userId);
        // 调用独立的事务方法执行upsert
        doUpsertInTransaction(userId, score);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void doUpsertInTransaction(Long userId, Integer score) {
        userScoreRepository.upsertScore(userId, score);
    }
}

这样事务只在执行DB操作时存在,锁持有时间大幅缩短,冲突概率自然降低。

5. 合理调整事务隔离级别

MySQL默认的REPEATABLE READ隔离级别会产生间隙锁,容易加剧死锁。如果你的业务能接受不可重复读的情况,可以降低到READ COMMITTED,缩小间隙锁范围:

在Spring Boot配置文件中添加:

spring.jpa.properties.hibernate.connection.isolation=2
# 2对应READ COMMITTED,4为默认的REPEATABLE READ

注意:调整前要确认业务逻辑不受影响,比如金额计算类场景需谨慎。

6. 增加死锁重试机制

即使做了以上优化,极端场景下仍可能出现死锁。你可以捕获MySQL的死锁异常(SQLState为40001),添加重试逻辑:

public void upsertWithRetry(Long userId, Integer score) {
    int retryCount = 3;
    while (retryCount > 0) {
        try {
            userScoreRepository.upsertScore(userId, score);
            return;
        } catch (DataAccessException e) {
            if (e.getRootCause() instanceof SQLException) {
                SQLException sqlEx = (SQLException) e.getRootCause();
                if ("40001".equals(sqlEx.getSQLState())) {
                    retryCount--;
                    log.warn("发生死锁,剩余重试次数:{}", retryCount);
                    // 重试前短暂休眠,避免立即冲突
                    Thread.sleep(100);
                } else {
                    throw e;
                }
            } else {
                throw e;
            }
        }
    }
    throw new RuntimeException("多次重试后仍因死锁失败");
}

内容的提问来源于stack exchange,提问作者Gremash

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:39:34