1. 并发控制的两种非Redis方案解析
在Web应用开发中,高并发场景下的数据一致性问题是每个开发者必须面对的挑战。虽然Redis常被作为首选方案,但在某些特定场景下(如基础设施限制、技术栈约束或性能考量),我们需要掌握不依赖Redis的并发控制方法。以下是经过实战验证的两种核心方案:
1.1 数据库悲观锁实现并发控制
悲观锁的核心思想是"先取锁再操作",适合写多读少的场景。以MySQL为例,最典型的实现方式是使用SELECT...FOR UPDATE语句:
sql复制BEGIN;
-- 对id=123的记录加排他锁
SELECT * FROM orders WHERE id = 123 FOR UPDATE;
-- 执行业务逻辑
UPDATE orders SET amount = amount - 100 WHERE id = 123;
COMMIT;
关键参数说明:
- 锁超时时间:通过innodb_lock_wait_timeout配置(默认50秒)
- 锁粒度:建议控制在行级锁,避免表锁导致性能瓶颈
实战经验:在电商库存扣减场景中,我们通过WHERE条件中增加库存校验(如
WHERE stock >= 100)实现原子操作,配合FOR UPDATE锁可完全避免超卖问题。
1.2 乐观锁的版本控制实现
乐观锁采用"先操作再验证"的思路,适合读多写少的场景。典型实现是在数据表中增加version字段:
java复制// 伪代码示例
public boolean updateWithOptimisticLock(Order order) {
int oldVersion = order.getVersion();
order.setVersion(oldVersion + 1);
int affectedRows = jdbcTemplate.update(
"UPDATE orders SET amount = ?, version = ? WHERE id = ? AND version = ?",
order.getAmount(), order.getVersion(), order.getId(), oldVersion
);
return affectedRows > 0;
}
性能优化技巧:
- 版本号可替换为时间戳(精度需到毫秒级)
- 失败后建议采用指数退避重试策略(如初始等待50ms,最大重试3次)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多定时任务管理的工程实践
2.1 Spring Boot中Quartz的多任务配置
常见问题"只执行最后一个任务"通常源于错误的Bean配置方式。正确做法是为每个Job定义独立的JobDetail和Trigger:
java复制@Configuration
public class QuartzConfig {
@Bean
public JobDetail job1Detail() {
return JobBuilder.newJob(OrderSyncJob.class)
.withIdentity("orderSyncJob")
.storeDurably()
.build();
}
@Bean
public Trigger job1Trigger() {
return TriggerBuilder.newTrigger()
.forJob(job1Detail())
.withIdentity("orderSyncTrigger")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/5 * * * ?"))
.build();
}
// 第二个任务需要创建新的JobDetail实例
@Bean
public JobDetail job2Detail() {
return JobBuilder.newJob(InventoryCheckJob.class)
.withIdentity("inventoryCheckJob")
.storeDurably()
.build();
}
}
关键配置项:
- misfire策略:建议设置为withMisfireHandlingInstructionDoNothing()
- 线程池大小:通过org.quartz.threadPool.threadCount配置(建议为任务数量的2-3倍)
2.2 线程池的精细化配置
线程池配置不当是导致并发问题的常见原因。以下是一个生产级配置示例:
java复制ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 常驻线程数(根据CPU核心数×2设置)
executor.setMaxPoolSize(50); // 最大线程数(建议不超过核心线程数×5)
executor.setQueueCapacity(100); // 队列容量(根据平均任务耗时调整)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setThreadNamePrefix("async-task-");
executor.setKeepAliveSeconds(60); // 空闲线程存活时间
executor.initialize();
容量计算公式参考:
code复制最大并发处理能力 = maxPoolSize + queueCapacity
系统最大并发量 ≈ QPS × 平均响应时间(s)
血泪教训:在支付对账系统中,我们曾因queueCapacity设置过大导致OOM,最终采用监控队列长度+动态调整的策略解决问题。
3. 连接池与WebDriver的并发管理
3.1 数据库连接池优化方案
连接池耗尽问题通常表现为"Too many connections"错误。以HikariCP为例的生产级配置:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20 # 建议值 = (核心数 * 2) + 有效磁盘数
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 5000
故障排查步骤:
- 监控活跃连接数:
SHOW STATUS LIKE 'Threads_connected' - 分析长事务:
SELECT * FROM information_schema.innodb_trx - 检查连接泄漏:开启leak-detection-threshold
3.2 WebDriver并发测试方案
针对Selenium WebDriver的并发测试,推荐采用独立Session模式:
java复制// 使用ThreadLocal保证每个线程独立的WebDriver实例
private ThreadLocal<WebDriver> driverThreadLocal = new ThreadLocal<>();
@Before
public void setUp() {
ChromeOptions options = new ChromeOptions();
options.setHeadless(true);
driverThreadLocal.set(new ChromeDriver(options));
}
@Test
public void testConcurrentOperation() {
WebDriver driver = driverThreadLocal.get();
driver.get("https://example.com");
// 测试逻辑...
}
@After
public void tearDown() {
if(driverThreadLocal.get() != null) {
driverThreadLocal.get().quit();
}
}
性能优化技巧:
- 使用Docker容器化浏览器实例
- 配置--shm-size参数避免内存不足
- 采用Grid模式实现分布式执行
4. 典型问题排查手册
4.1 线程池常见异常处理
| 异常现象 | 根本原因 | 解决方案 |
|---|---|---|
| 任务被静默丢弃 | 队列满且拒绝策略为DiscardPolicy | 改用CallerRunsPolicy或记录日志 |
| CPU利用率100% | 核心线程数设置过高 | 调整为CPU核心数×2 |
| 任务执行延迟大 | 队列容量过大导致排队 | 监控队列长度并动态调整 |
4.2 定时任务踩坑记录
-
时间不同步问题:
- 现象:分布式节点任务重复执行
- 解决:采用数据库行锁或Redis分布式锁
-
长周期任务阻塞:
- 现象:后续任务无法按时启动
- 解决:配置@DisallowConcurrentExecution注解
-
Spring事务失效:
- 现象:任务方法内异常未回滚
- 解决:在方法内显式调用TransactionTemplate
java复制// 正确的事务控制示例
@Scheduled(fixedRate = 5000)
public void scheduledTask() {
transactionTemplate.execute(status -> {
// 业务逻辑
return null;
});
}
在金融级系统中,我们最终采用分片广播+幂等设计的方案,通过XXL-JOB实现每天千万级交易记录的分布式处理,关键点在于:
- 每个分片处理特定范围数据(如按ID取模)
- 任务日志记录处理偏移量
- 失败分片自动重试机制
