在Java企业级系统中,多用户并发访问数据库是日常必须面对的课题。当多个线程或分布式节点同时对同一批数据发起查询、插入或更新时,如果缺乏合理设计,就会引发连接争用、数据不一致、死锁甚至服务雪崩。要从工程层面解决这些问题,需要从连接资源管控、事务语义界定以及并发控制手段三个维度系统性地构建策略。

数据库连接池的合理配置与复用
多用户并发的第一道瓶颈往往是物理连接数。Java应用通常不会为每个请求直接创建数据库连接,而是借助连接池将连接复用起来。主流方案如HikariCP、Druid都通过预先建立一定数量的连接,避免频繁握手带来的开销。如果池子太小,大量线程在getConnection处阻塞;如果太大,数据库侧进程调度与内存压力又会陡增。一般建议根据数据库最大连接限制与单实例承载量,将应用侧池上限控制在数据库总连接的百分之六十到七十。
除了容量,连接存活策略也影响并发稳定性。需要设置合理的maxLifetime小于数据库主动断连时间,并开启心跳保活。以下示例展示HikariCP的基础配置方式:
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/demo");
config.setUsername("root");
config.setPassword("password");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setMaxLifetime(1800000);
config.setConnectionTimeout(30000);
HikariDataSource dataSource = new HikariDataSource(config);
在微服务架构下,还要考虑多实例部署带来的总连接放大效应。假设单应用池上限二十,十个实例就会向数据库申请两百连接。此时应结合数据库实际规格做全局规划,必要时引入ProxySQL或ShardingSphere做连接收敛。连接池监控也不可少,通过暴露等待线程数与超时计数,可以在并发洪峰前及时发现资源不足。
事务隔离级别与并发可见性控制
多用户同时读写时,事务隔离级别决定了彼此能看到什么中间状态。Java通过JDBC的Connection.setTransactionIsolation来设定,可选值包括读未提交、读已提交、可重复读与串行化。MySQL默认是可重复读,而Oracle常见为读已提交。级别越低并发性能越好,但可能出现脏读或幻读;级别越高一致性越强,却会带来更多锁等待与吞吐下降。
实践中不应统一使用串行化,而应按业务敏感度分级。比如账户余额变更必须避免丢失更新,可采用读已提交配合乐观锁;报表类查询允许一定滞后,则可使用读已提交并加上只读事务提示。下面代码演示在Spring中声明式指定隔离级别:
@Service
public class OrderService {
@Transactional(isolation = Isolation.READ_COMMITTED, readOnly = false)
public void updateStock(Long itemId, int count) {
Item item = itemMapper.selectById(itemId);
item.setStock(item.getStock() - count);
itemMapper.updateById(item);
}
}
还要注意事务边界过宽的问题。很多开发者把远程调用或文件处理放进数据库事务里,导致连接长时间不释放,在并发下迅速拖垮池子。正确做法是将事务限制在纯数据库操作内,外部交互采用最终一致性或消息队列解耦。对于跨库操作,尽量避免在应用层用本地事务硬撑,而应引入柔性事务框架。
乐观锁与悲观锁的场景化选型
当多个用户并发修改同一行记录,直接更新极易造成后写覆盖。悲观锁通过select ... for update在数据库层加行锁,适合冲突频繁且业务逻辑重的场景,但会阻塞其他读线程。乐观锁则在表里增加版本字段,更新时校验版本,冲突则重试或抛异常,适合读多写少。Java实现乐观锁通常在SQL中带上版本条件:
update item set stock = stock - 1, version = version + 1 where id = 100 and version = 3;
在代码侧可用MyBatis-Plus的@Version注解自动注入版本比对。若更新返回零行,说明版本已变,应捕获异常引导用户重新操作。悲观锁示例则需在事务内显式加锁:
@Transactional
public void deductWithPessimistic(Long id) {
Item item = jdbcTemplate.queryForObject(
"select * from item where id = ? for update",
new Object[]{id}, new ItemRowMapper());
jdbcTemplate.update("update item set stock = stock - 1 where id = ?", id);
}
选型时还要考虑重试代价。乐观锁在秒杀类极端冲突下会导致大量重试请求,此时可结合库存分段或Redis预扣减削峰。悲观锁虽稳却牺牲了并发度,仅建议用在金额结算等强一致核心链路。无论哪种锁,都必须保证索引命中加锁行,否则MySQL会锁升级为表锁,让多用户并发直接退化为串行。
批量处理与索引优化降低锁竞争
并发访问性能不只看锁本身,也取决于每次持锁做多少事。将单条循环插入改为批量提交,能成倍减少网络往返与日志刷盘。JDBC的addBatch与executeBatch配合rewriteBatchedStatements参数,在MySQL下可显著提升吞吐。以下片段展示批量写入:
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
PreparedStatement ps = conn.prepareStatement("insert into log(uid,action) values(?,?)");
for (int i = 0; i < 1000; i++) {
ps.setLong(1, i);
ps.setString(2, "click");
ps.addBatch();
}
ps.executeBatch();
conn.commit();
索引设计同样关键。缺少恰当索引时,并发更新会触发全表扫描并放大锁范围。应通过慢查询日志与执行计划,确认高频并发语句走的是主键或唯一索引。对于多列过滤条件,建立联合索引并注意最左匹配。同时避免在大事务中更新过多行,将批量任务拆成小批次,既能控制锁时长,也方便失败续传。
综合来看,Java应用的多用户并发数据库访问不是单点技术,而是连接、事务、锁与SQL质量的合力。先在连接池层留足余量并监控,再按业务定隔离级别,接着在冲突点用对锁模型,最后用批量和索引压缩资源占用,才能在高并发下既稳又快。