问题发现场景:
项目中用了@Scheduled(fixedDelay = 10000)注解每10s循环更细数据库,每次更新差不多两万条数据吧。多个方法分别操作数一个表中不同clusterName字段标识的行,开启事务@Transactional(rollbackFor = Exception.class)后,开启线程池@Async("asyncPostFix")做异步更新数据表操作。今天看了日志发现出现了这个锁等待超时的错误。

开启线程池:

@Configuration
public class AsyncConfig {

    private static final int MAX_POOL_SIZE = 50;
    private static final int CORE_POOL_SIZE = 20;

    @Bean("asyncPostFix")
    public AsyncTaskExecutor asyncPostFix() {
        ThreadPoolTaskExecutor asyncPostFix = new ThreadPoolTaskExecutor();
        asyncPostFix.setMaxPoolSize(MAX_POOL_SIZE);
        asyncPostFix.setCorePoolSize(CORE_POOL_SIZE);
        asyncPostFix.setThreadNamePrefix("async-task-thread-pool-");
        asyncPostFix.initialize();
        return asyncPostFix;
    }

}

官方文档:

Error: 1205 SQLSTATE: HY000 (ER_LOCK_WAIT_TIMEOUT)
Message: Lock wait timeout exceeded; try restarting transaction
InnoDB reports this error when lock wait timeout expires. The statement that waited too long was rolled back (not the entiretransaction). You can increase the value of the innodb_lock_wait_timeout configuration option if SQL statements should wait longer for other transactions to complete, or decrease it if too many long-running transactions are causing locking problems and reducing concurrency on a busy system.
The length of time in seconds an InnoDB transaction waits for a row lock before giving up. The default value is 50 seconds. A transaction that tries to access a row that is locked by another InnoDB transaction waits at most this many seconds for write access to the row before issuing the following error:
ERROR 1205 (HY000):Lock wait timeout exceeded; try restarting transaction

翻译如下:
当锁等待超时后innodb引擎报此错误,等待时间过长的语句被回滚(不是整个事务)。如果想让SQL语句等待其他事务更长时间之后完成,你可以增加参数innodb_lock_wait_timeout配置的值。如果有太多长时间运行的有锁的事务,你可以减小这个innodb_lock_wait_timeout的值,在特别繁忙的系统,你可以减小并发。
InnoDB事务等待一个行级锁的时间最长时间(单位是秒),超过这个时间就会放弃。默认值是50秒。一个事务A试图访问一行数据,但是这行数据正在被另一个innodb事务B锁定,此时事务A就会等待事务B释放锁,等待超过innodb_lock_wait_timeout设置的值就会报错ERROR 1205 (HY000):

会引发锁等待超时的场景:

  1. 因为是多个事务同时更新一张表,所以可能是当前事务在等待其他事务释放锁资源造成的。这个只能找到竞争资源的表,然后尽量优化sql,或者把方法集中到一个事务里。或者减少并发执行的线程数量。
  2. 某个事务在等待给某个表加锁时超时了。可能是表在被另一个进程锁住没有及时释放。
  3. 发生死锁,innodb会自动监测死锁并进行回滚,或者终止死锁
  4. 在同一事务内先后对同一条数据进行插入和更新操作
  5. 瞬时出现高并发现象,spring事务造成数据库死锁,后续操作超时抛出异常
  6. 事务A对记录C进行更新/删除操作的请求未commit时,事务B也对记录C进行更新/删除操作。此时,B会等A提交事务,释放行锁。当等待时间超过innodb_lock_wait_timeout设置值时,会产生“LOCK WAIT”事务。

看有无一直没有提交的事务,找到对应线程手动kill,没有发现这种一直未提交的事务,所以我很确定不是因为死锁引发的这个问题,所以一定是资源未及时释放导致的超时。

因为innodb事务开启后等待锁的默认时间为1.2s,而我的项目中是每10s同时执行三个事务去异步更新操作一张表,所以可能是在这10s内有某一个事务没有执行完毕,导致后续的一个10s再次操作表时资源被占用没有及时释放,也就是说在第11.2s的时候锁还没有被释放,所以造成了这个锁等待超时的问题。

我采取了几个措施:

  1. 开启异步执行,减少并发线程数量
  2. 修改innodb默认的锁等待超时时间,将50s更改为60s@Transactional(rollbackFor = Exception.class,timeout = 60),项目中每10s执行三个更新事务,那么等待超时时间为10s后,就不可能会出现锁等待超时的情况。
  3. 修改定时执行时间,将10s执行改为13s,因为原10s的运行log中我发现只有第一个定时任务会发生这种锁等待超时现象,所以我推断,既然只有第一个会引发超时,那么就是说这个超时时间虽然大于51.2s,但是一定小于52.4s,也就是说不会引发第二个超时,所以更改为13s执行加上10s的超时时间就可以万全杜绝这个锁等待超时问题,果然在发布新版本后,不再出现这个问题了。

有几个预防措施:

  1. 在使用@transtion开启事务的时候,可以指定超时时间@Transactional(rollbackFor = Exception.class,timeout = 60)
  2. 事务中存在批量修改、删除语句的时候,where条件尽量加索引
  3. 事务中存在批量修改、删除语句的时候,尽可能减少事务的执行时间