数据库死锁问题

之前在开发的时候遇到一个需求,需要批量对数据库操作,实现代码如下:

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements IProductService {
    @Override
    @Transactional(rollbackFor = Exception.class, transactionManager = "transactionManager")
    public List<ProductVO> getProductByDate(List<String> dates) {
        for (String date : dates) {   
        	// 从其它地方获取信息
            List<ProductEntity> productEntityList = getProduct();
            ......
            // save to database
            if (!productEntityList.isEmpty()) {
                LambdaQueryWrapper<ProductEntity> queryWrapper = new LambdaQueryWrapper<>();
                queryWrapper.eq(ProductEntity::getDate, date);
               	remove(queryWrapper);
               	saveBatch(productEntityList);
            }
        }
        return ProductVO;
    }
}

这里是根据时间批量处理数据,所以当数据库需要处理操作量大、复杂度高的数据的时候需要用到事务。用事务是为了保证数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。

建表语句:

CREATE TABLE `product` (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `date` char(7) DEFAULT NULL COMMENT '时间:date',
  ......
  `create_time` bigint(20) DEFAULT NULL COMMENT '创建时间',
  `create_time` bigint(20) DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='product表';

当代码部署到开发环境以后,发现出现了数据库死锁问题,报错信息如下:

2021-09-13 00:10:14,105--[TID: N/A]-- [scheduling-1] ERROR org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler  - Unexpected error occurred in scheduled task
org.springframework.dao.DeadlockLoserDataAccessException: 
### Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may exist in com/cloud/software/mapper/productMapper.java (best guess)
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: DELETE FROM product     WHERE (date= ?)
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
	at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:267)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
	at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:88)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440)
	at com.sun.proxy.$Proxy163.delete(Unknown Source)

经过网上查阅得知造成死锁是因为:在事务中用for循环更新一张表,这张表中有主键和二级索引,更新就是以二级索引为条件,这时候,因为for循环里面执行的循序不一定,所以有可能导致死锁。

在这个方法加了@Transactional注解,表示调用这个方法时对数据库进行事务操作,这个注解只能加在public 方法上,并且如果这个方法内部发生异常就会事务回滚,然后我通过for循环对数据库进行删除和保存操作,在批量删除的时候是以 date字段为查询条件, date字段是二级索引。

为什么这样会导致死锁呢?
因为查询的时候是以二级索引为条件的,数据库在查询的时候会先通过二级索引找到主键,然后才通过主键进行相应的操作,再加上这是批量操作,可能会造成第一个事务拿到 date索引,锁定主键id,第二个索引拿到主键id索引,锁定数据记录,造成互相等待对方释放锁资源,从而相互等待,造成死锁。

解决办法:
可以将更新数据库的内容单独抽取出来,进行事务处理,或者将二级索引找主键回表操作手动拆分出来。
我是将这两个方法一起使用了,代码如下:

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements IProductService {
	@Resource
    private ProductMapper productMapper;

    @Resource
    private ApplicationContext applicationContext;
	
    @Override
    public List<ProductVO> getProductByDate(List<String> dates) {
    	IProductService iProductService = applicationContext.getBean(IProductService.class);
        for (String date : dates) {   
        	// 从其它地方获取信息
            List<ProductEntity> productEntityList = getProduct();
            ......
            // save to database
            if (!productEntityList.isEmpty()) {
                LambdaQueryWrapper<ProductEntity> queryWrapper = new LambdaQueryWrapper<>();
                queryWrapper.eq(ProductEntity::getDate, date);
               	List<ProductEntity> productEntities = productMapper.selectList(queryWrapper);
                iProductService.updateData(productEntities.stream().map(ProductEntity::getId).distinct().collect(Collectors.toList()), productEntityList);
            }
        }
        return ProductVO;
    }

	@Transactional(rollbackFor = Exception.class, transactionManager = "transactionManager")
    @Override
    public void updateData(List<Long> existIds, List<productEntity> productEntityList) {
        if (CollectionUtil.isEmpty(productEntityList)) {
            return;
        }
        if (CollectionUtil.isNotEmpty(existIds)) {
            removeByIds(existIds);
        }
        saveBatch(productEntityList);
    }
}

这样就解决了死锁问题。

这里需要注意一点: 通过 Spring 官方文档可以知道:在默认的代理模式下,只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。在同一个类中的两个方法直接调用,是不会被 Spring 的事务拦截器拦截。例如:我这里updateData方法仅供内部调用,并且使用了@Transactional事务管理,所以我就需要将该类交给Spring管理:

IProductService iProductService = applicationContext.getBean(IProductService.class);

这里提供的是该类的接口:
然后通过iProductService.updateData调用,如果是供外部调用的则不需要这样做。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值