场景描述
在一个线程中将一批任务通过循环依次添加到LinkedBlockingQueue中,并将任务状态改为待执行。后台使用mysql数据库。然后另外有一个线程从这个queue中阻塞取出任务,并执行,这时将任务状态修改为执行中。
@Transactional
public void taskBatchSchedule(List<Integer>ids) {
for (Integer id: ids) {
Task task = getTask(id);
queue.put(task);
updateTaskWait(task);
}
}
问题描述
在这个场景下,任务状态的变更有时候正常,有时出现问题:第一个任务状态一直已经执行但状态还为待执行,执行结束后任务状态修改为执行结束。接着第二个任务正常显示任务状态。
问题分析
这里有生产和消费两个线程,当生产线程循环去添加任务并连接数据库更新任务状态。当生产端还没结束,事务还未提交(一个方法视为一个事务)。此时,消费端已经取出任务队列中第一个任务去执行。当它去更改任务状态时,由于生产线程的事务还未提交。消费线程等待超时后便继续向后执行,任务状态修改失败。
在我这个场景中,第一个任务执行的时候最容易出现该问题,因为此时生产和消费线程同时竞争数据库资源,第二,或者后面的任务在执行的时候,生产线程已经结束,就不太会出现这个问题。
相关知识总结
事务范围
- 当方法上标注了@Transactional注解时,整个方法体内的所有数据库操作都会在一个统一的事务范围内进行。
- 这意味着事务会在方法执行开始时开启,并在方法正常完成时提交,或者在方法中抛出未被捕获的异常时回滚。
循环中的更新
- 如果在一个循环中更新多条记录,这些更新操作都会在同一事务中进行。
- 即使循环中有多个数据库操作,也只有在循环结束后整个方法执行完成时,事务才会提交。
锁和并发
- 在循环中更新数据时,如果其他线程尝试同时更新相同的记录,那么这些线程将等待锁释放,或者因为锁等待超时而失败。
- MySQL默认使用行级锁,在InnoDB存储引擎中,当一个事务正在更新一行数据时,它会对那行数据加上排他锁(X锁),阻止其他事务同时更新该行数据。
超时和异常处理
- 通过@Transactional(timeout = seconds)来设置事务的最大执行时间。如果事务在此时间内没有完成,Spring会自动回滚事务并抛出异常。
- 如果事务因为长时间等待锁而超时,MySQL会抛出LockWaitTimeoutException,Spring会将其转换为TransactionSystemException异常。捕获并处理TransactionSystemException异常,以确保应用程序能够优雅地处理这种情形。调整MySQL的innodb_lock_wait_timeout系统变量来配置数据库级别的锁等待超时时间。
问题解决
问题在于两个线程资源争夺所造成,所以这里我使用了ReentrantLock来同步生产和消费线程。另外,将生产线程里,循环更新任务状态改为批量更新,特别是在数据批次较大的时候,能够显著减少数据库操作次数,减少事务持续时间。
愿你我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意。