之前在开发的时候遇到一个需求,需要批量对数据库操作,实现代码如下:
@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
调用,如果是供外部调用的则不需要这样做。