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

Spring Boot多实例与MySQL并发问题解决方案咨询

问题描述

我将Spring Boot应用部署在两个实例上,使用MySQL作为数据库。当发送两条携带相同message的并发请求时,出现以下问题:

  1. 实例1执行如下逻辑:
Item item = itemRepository.findByMessage(message);
if (item != null) { 
  item.setDate(date);
} else {
   Item newItem = new Item();
   newItem.setMessage(message); // message是唯一标识
   newItem.setDate(date);
   try {
     itemRepository.save(newItem);
   } catch (DataIntegrityViolationException e) {
       if (isDuplicateEntryException(e)) {
       itemUpdateService.updateItemIfFailure(message, date);
       }
   }
  }
  1. 实例2同时执行相同请求,由于未检测到实例1插入的条目,也尝试插入,导致唯一键约束冲突(Duplicate Entry异常)。
  2. 为处理该情况,我在独立事务方法中实现了更新操作,使用@Transactional(propagation = Propagation.REQUIRES_NEW):
@Transactional(propagation = Propagation.REQUIRES_NEW)
public long updateItemIfFailure(Date date, String message) {
  Item item = itemRepository.findByMessage(message);
  item.setDate(date);
  itemRepository.save(item);
}

但这会引发锁等待超时错误:

Lock wait timeout exceeded; try restarting transaction

问题:如何避免两个实例间的并发问题,在不触发唯一键冲突或锁等待超时的前提下,正确处理条目的插入或更新?


解决方案

方案1:用MySQL原子操作INSERT ... ON DUPLICATE KEY UPDATE(推荐)

这是最直接高效的方案,把插入/更新的逻辑交给数据库原子处理,彻底避免应用层的竞态问题。

前提是message字段已经在MySQL表中设置了唯一索引(你已经满足这个条件)。在Repository中定义原生SQL方法:

public interface ItemRepository extends JpaRepository<Item, Long> {
    @Modifying
    @Transactional
    @Query(value = "INSERT INTO item (message, date) VALUES (:message, :date) ON DUPLICATE KEY UPDATE date = :date", nativeQuery = true)
    void insertOrUpdate(@Param("message") String message, @Param("date") Date date);
}

然后业务逻辑直接简化成调用这个方法就行,不用再写查询判断和异常捕获:

itemRepository.insertOrUpdate(message, date);

MySQL会自动处理:如果该message的记录不存在就插入,存在则更新date字段,全程原子性,不会有冲突。

方案2:使用悲观锁(SELECT ... FOR UPDATE)

如果不想写原生SQL,可以在查询时加排他锁,确保同一时间只有一个事务能操作目标记录。

修改Repository的查询方法,加上悲观锁注解:

public interface ItemRepository extends JpaRepository<Item, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<Item> findByMessage(String message);
}

然后把业务逻辑放在同一个事务里执行:

@Transactional
public void handleItem(String message, Date date) {
    Optional<Item> itemOpt = itemRepository.findByMessage(message);
    if (itemOpt.isPresent()) {
        Item item = itemOpt.get();
        item.setDate(date);
        itemRepository.save(item);
    } else {
        Item newItem = new Item();
        newItem.setMessage(message);
        newItem.setDate(date);
        itemRepository.save(newItem);
    }
}

PESSIMISTIC_WRITE会在查询时给对应行加排他锁,其他事务必须等锁释放才能操作,避免了并发插入的问题。注意必须保证整个逻辑在同一个事务中,否则锁会提前释放。

方案3:应用层加分布式锁

如果需要更灵活的控制,或者数据库锁满足不了场景,可以用分布式锁(比如Redis),确保同一时间只有一个实例能处理特定message的请求。

这里用Redisson做示例:

@Autowired
private RedissonClient redissonClient;

@Autowired
private ItemRepository itemRepository;

public void handleItem(String message, Date date) {
    // 用message作为锁的唯一标识
    RLock lock = redissonClient.getLock("item_lock_" + message);
    try {
        // 尝试获取锁,最多等5秒,持有锁10秒
        if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
            // 执行原来的查询-插入/更新逻辑
            Optional<Item> itemOpt = itemRepository.findByMessage(message);
            if (itemOpt.isPresent()) {
                Item item = itemOpt.get();
                item.setDate(date);
                itemRepository.save(item);
            } else {
                Item newItem = new Item();
                newItem.setMessage(message);
                newItem.setDate(date);
                itemRepository.save(newItem);
            }
        } else {
            // 获取锁失败,可根据业务需求重试或返回提示
            throw new RuntimeException("当前请求繁忙,请稍后再试");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("请求被中断");
    } finally {
        // 确保锁被释放
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

这种方式完全隔离了并发请求,但需要额外引入分布式锁组件,适合复杂的多实例并发场景。


为什么原来的方案会出问题?

你用REQUIRES_NEW开启新事务的方式,会导致实例1插入数据未提交时,实例2触发Duplicate异常后进入新事务,尝试查询更新,但此时实例1的事务还握着该行的锁,实例2的新事务会一直等锁,超过MySQL默认的锁等待超时时间(50秒)就会抛出锁等待超时错误。而上面的方案要么用数据库原子操作,要么用更合理的锁策略,都能避免这个问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 19:16:04