JDBC多线程场景下数据库数据异常问题求助
解决JDBC多线程战斗场景数据异常与并行问题的方案
一、替换本地锁为数据库行级锁+事务
Java进程内的ReentrantLock无法跨连接保证数据库操作的原子性,多线程下容易出现脏读、丢失更新。正确的做法是利用数据库的行级锁和事务来控制:
- 查询参战Clan时使用
SELECT ... FOR UPDATE语句,让数据库锁定目标行,其他线程必须等待当前事务提交后才能操作该行; - 将战斗计算、金库更新、Event插入整个流程放在同一个JDBC事务中,设置
conn.setAutoCommit(false),所有操作完成后统一commit(),异常则rollback(),避免部分操作生效导致数据不一致。
二、规范JDBC连接与事务边界
- 每个线程必须使用独立的Connection,不要共享连接;
- 事务边界要统一在战斗逻辑的最外层(比如ActionService的fight方法),不要在Repository层随意开启/提交事务;
- 操作完成后务必关闭Connection,避免连接泄漏。
三、用乐观锁实现无锁并行(优化并行度)
如果想减少锁等待、提升多线程并行效率,可采用乐观锁方案:
- 在Clan表新增
version字段,每次更新时校验版本号:UPDATE clan SET gold = ?, version = version + 1 WHERE id = ? AND version = ?; - 若更新返回受影响行数为0,说明数据已被其他线程修改,此时可重试战斗逻辑(注意设置重试次数上限,避免死循环)。这种方式无需显式加锁,冲突不频繁时能大幅提升并行能力。
四、异常排查要点
- 开启数据库SQL日志(如MySQL的general_log),查看SQL执行顺序、事务提交时机,定位是脏读、丢失更新还是部分提交;
- 检查JDBC事务隔离级别,MySQL默认的
REPEATABLE READ已能解决大部分一致性问题,无需随意修改。
代码示例片段
Service层战斗方法
public void fight(Long attackerId, Long defenderId) { Connection conn = null; try { conn = getConnection(); // 每个线程获取独立连接 conn.setAutoCommit(false); // 加行级锁查询双方Clan Clan attacker = clanRepository.getByIdForUpdate(conn, attackerId); Clan defender = clanRepository.getByIdForUpdate(conn, defenderId); // 执行战斗计算 int gain = calculateBattleGain(defender); attacker.setGold(attacker.getGold() + gain); defender.setGold(defender.getGold() - gain); // 更新金库 clanRepository.update(conn, attacker); clanRepository.update(conn, defender); // 插入战斗事件 Event battleEvent = new Event(attackerId, defenderId, gain); eventRepository.insert(conn, battleEvent); conn.commit(); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException("战斗执行失败", e); } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
Repository行级锁查询方法
public Clan getByIdForUpdate(Connection conn, Long id) throws SQLException { String sql = "SELECT id, gold FROM clan WHERE id = ? FOR UPDATE"; try (PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setLong(1, id); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { Clan clan = new Clan(); clan.setId(rs.getLong("id")); clan.setGold(rs.getInt("gold")); return clan; } throw new RuntimeException("Clan不存在"); } } }
乐观锁更新方法(可选)
public boolean updateWithOptimisticLock(Connection conn, Clan clan) throws SQLException { String sql = "UPDATE clan SET gold = ?, version = version + 1 WHERE id = ? AND version = ?"; try (PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setInt(1, clan.getGold()); stmt.setLong(2, clan.getId()); stmt.setInt(3, clan.getVersion()); return stmt.executeUpdate() > 0; } }
内容的提问来源于stack exchange,提问作者Igor Meshalkin
相关产品推荐
相关产品推荐

