1. 项目概述
最近在重构一个后台管理系统时,遇到了两个典型的并发问题:一是订单处理时的并发控制,二是需要同时运行多个定时任务。经过几轮方案对比和测试,最终在没有引入Redis的情况下,通过纯Java方案解决了这些问题。今天就把这些实战经验分享给大家,特别是那些受限于环境无法使用Redis的团队。
这个方案的核心在于合理利用Java并发包和线程池机制。我们不仅解决了数据一致性问题,还实现了定时任务的灵活管理。下面我会从问题场景、解决方案到具体实现,一步步拆解整个过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并发控制方案解析
2.1 数据库层面并发控制
在没有Redis的情况下,我们首先考虑的是数据库自带的并发控制机制。MySQL的InnoDB引擎提供了行级锁机制,这是我们解决并发问题的第一道防线。
悲观锁实现方案:
java复制// 使用SELECT...FOR UPDATE实现悲观锁
@Transactional
public void processOrder(Long orderId) {
Order order = orderMapper.selectForUpdate(orderId); // 关键行锁定
if (order.getStatus() == Status.UNPAID) {
order.setStatus(Status.PROCESSING);
orderMapper.update(order);
// 业务处理逻辑...
}
}
注意:使用悲观锁时务必注意锁的粒度,过大的锁范围会导致性能急剧下降。我们曾经因为锁了整个表导致系统吞吐量从1000TPS降到50TPS。
乐观锁实现方案:
java复制// 使用version字段实现乐观锁
public boolean updateWithOptimisticLock(Order order) {
int affectedRows = orderMapper.updateWithVersion(
order.getId(),
order.getVersion(),
Status.PROCESSING);
return affectedRows > 0;
}
两种锁方案的对比:
| 方案类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 悲观锁 | 冲突频率高 | 实现简单 | 性能开销大 |
| 乐观锁 | 冲突频率低 | 无锁竞争 | 需要重试机制 |
2.2 Java并发工具类应用
除了数据库锁,Java并发包中的工具类也是解决并发问题的利器。我们主要使用了以下几种:
- ReentrantLock可重入锁
java复制private final Lock lock = new ReentrantLock();
public void process() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
- Semaphore信号量
java复制// 限制同时处理的请求数量
private Semaphore semaphore = new Semaphore(10);
public void handleRequest() {
semaphore.acquire();
try {
// 业务处理
} finally {
semaphore.release();
}
}
- CountDownLatch任务协调
java复制// 等待多个子任务完成
public void batchProcess(List<Long> ids) {
CountDownLatch latch = new CountDownLatch(ids.size());
ids.forEach(id -> executor.execute(() -> {
try {
processSingle(id);
} finally {
latch.countDown();
}
}));
latch.await(30, TimeUnit.SECONDS);
}
实操心得:ReentrantLock比synchronized更灵活,支持尝试获取锁、超时机制等。但在简单场景下,synchronized的性能其实更好,因为JVM对其有优化。
3. 线程池配置与优化
3.1 线程池核心参数解析
正确的线程池配置对系统稳定性至关重要。以下是我们的生产环境配置经验:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
20, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 任务队列
new NamedThreadFactory("order-process"), // 线程命名
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
各参数设置依据:
- 核心线程数:根据CPU核心数设置,通常为CPU核数+1
- 最大线程数:考虑IO密集型/CPU密集型任务特点
- 队列容量:根据系统内存和任务特点决定
- 拒绝策略:生产环境推荐CallerRunsPolicy
3.2 线程池监控与调优
我们通过自定义ThreadPoolExecutor实现了监控功能:
java复制public class MonitorableThreadPool extends ThreadPoolExecutor {
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
// 记录任务开始时间
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// 统计任务执行时间
}
}
监控指标包括:
- 活跃线程数
- 队列积压任务数
- 任务平均耗时
- 拒绝任务数
踩坑记录:曾经因为队列设置过大导致OOM,后来通过压测确定了合理的队列大小。建议队列容量不要超过1000,同时配合合适的拒绝策略。
4. 多定时任务管理方案
4.1 Quartz定时任务框架
Spring Boot集成Quartz的配置要点:
java复制@Configuration
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setJobFactory(springBeanJobFactory());
factory.setQuartzProperties(quartzProperties());
return factory;
}
// 其他配置...
}
多任务配置示例:
java复制public void scheduleJob(Scheduler scheduler, Class<? extends Job> jobClass,
String jobName, String group, String cron) throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(jobName, group)
.storeDurably()
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName + "Trigger", group)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
4.2 @Scheduled注解的进阶用法
Spring自带的@Scheduled注解也可以实现多任务管理:
java复制@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(10);
}
}
任务定义示例:
java复制@Component
public class ReportTasks {
@Scheduled(cron = "0 0 2 * * ?")
public void generateDailyReport() {
// 日报生成逻辑
}
@Scheduled(fixedRate = 300000)
public void syncData() {
// 数据同步逻辑
}
}
常见问题:多个@Scheduled任务默认使用单线程执行,必须配置TaskScheduler才能并行执行。我们曾经因为这个问题导致任务堆积,后来通过配置线程池解决。
5. 连接池优化实践
5.1 数据库连接池配置
HikariCP生产配置示例:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
关键参数说明:
- maximum-pool-size:根据数据库服务器配置设置
- connection-timeout:网络不稳定时可适当增大
- idle-timeout:避免连接泄漏
5.2 连接泄漏排查
我们通过以下方式监控连接泄漏:
java复制@Bean
public DataSource dataSource() {
HikariDataSource ds = DataSourceBuilder.create().type(HikariDataSource.class).build();
ds.setLeakDetectionThreshold(30000); // 30秒泄漏检测
return ds;
}
常见泄漏场景:
- 未关闭ResultSet/Statement
- 事务未正确结束
- 线程池任务卡住不释放连接
避坑指南:连接池大小不是越大越好。我们曾经设置过100的连接池,结果导致数据库连接数爆满。经过压测,最终确定20是最佳值。
6. WebDriver并发处理
6.1 WebDriver多实例管理
对于需要并发执行Web自动化测试的场景,我们实现了WebDriver池:
java复制public class WebDriverPool {
private BlockingQueue<WebDriver> driverQueue;
public WebDriverPool(int size) {
driverQueue = new LinkedBlockingQueue<>(size);
for (int i = 0; i < size; i++) {
driverQueue.add(createNewDriver());
}
}
public WebDriver getDriver() throws InterruptedException {
return driverQueue.poll(30, TimeUnit.SECONDS);
}
public void releaseDriver(WebDriver driver) {
driverQueue.offer(driver);
}
}
6.2 WebDriver复用优化
为了避免频繁创建销毁WebDriver实例,我们实现了会话复用:
java复制public class WebDriverWrapper implements AutoCloseable {
private WebDriver driver;
public WebDriverWrapper(WebDriverPool pool) {
this.pool = pool;
this.driver = pool.getDriver();
}
@Override
public void close() {
// 清理会话状态
driver.manage().deleteAllCookies();
pool.releaseDriver(driver);
}
}
使用方式:
java复制try (WebDriverWrapper wrapper = new WebDriverWrapper(pool)) {
WebDriver driver = wrapper.getDriver();
// 执行测试逻辑
} // 自动释放
性能对比:使用连接池后,Web自动化测试的执行时间从平均3分钟降到40秒,资源消耗减少70%。
7. 常见问题与解决方案
7.1 并发问题排查清单
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数据不一致 | 并发更新未加锁 | 添加乐观锁/悲观锁 |
| 系统响应慢 | 线程池队列积压 | 调整线程池参数 |
| 连接池耗尽 | 连接泄漏 | 启用泄漏检测 |
| 定时任务不执行 | 单线程阻塞 | 配置任务线程池 |
7.2 性能优化经验
-
线程池大小公式:
- CPU密集型:核心数 + 1
- IO密集型:核心数 * (1 + 平均等待时间/平均计算时间)
-
连接池监控指标:
- 活跃连接数
- 等待获取连接的线程数
- 连接获取平均耗时
-
锁优化技巧:
- 减小锁粒度
- 使用读写锁替代独占锁
- 考虑无锁数据结构
经过这些优化,我们的系统在双十一大促期间成功支撑了每秒5000+的订单处理量,且没有使用Redis等外部缓存,完全依靠JVM层面的优化实现了高并发处理。
