问题描述
在MongoDB中,当多个事务同时修改同一个document数据时,就会有事务冲突,后面的事务检测到有其他的事务正在修改同一个document数据时,就会抛出WriteConflict写冲突异常,不会执行这个事务。
bug复现
- 1.编写
Controller类如下:
/**
* @author: huangshuai
* @Description
* @date 2021/12/20 10:20
*/
@RestController
public class TestAController {
@Autowired
private TestService testService;
private static final Logger log = LoggerFactory.getLogger(TestAController.class);
@RequestMapping("/testA")
public String testA() {
log.info("开始执行A");
if (testService.handleA()) {
return "A修改成功";
}else {
return "A修改失败";
}
}
}
- 2.编写
Service类如下:
/**
* @author: huangshuai
* @Description
* @date 2021/12/20 10:21
*/
@Service
public class TestService {
@Autowired
@Qualifier("interactionMongoTemplate")
private MongoTemplate mongoTemplate;
@Transactional(value = "mongodbTransactionManager",propagation = Propagation.REQUIRED)
public Boolean handleA() {
Query query = new Query(new Criteria().andOperator(Criteria.where("_id").is("xxxxxx")));
Update update = new Update();
System.out.println("当前处理线程:" + Thread.currentThread().getName());
update.set("待更新字段名", Thread.currentThread().getName());
UpdateResult updateResult = mongoTemplate.updateMulti(query, update, "表名xxxxx");
return updateResult.getModifiedCount() > 0;
}
}
- 3.使用
Jmeter测试,Jmeter的配置如下图所示
- 4.运行,复现的效果如下图所示
当前处理线程:qtp2126392903-97
2021-12-20 12:15:40.283 WARN 20448 --- [tp2126392903-99] org.eclipse.jetty.server.HttpChannel : /testA
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.data.mongodb.UncategorizedMongoDbException: Command failed with error 112 (WriteConflict): 'WriteConflict' on server xxxxxxx
分析
Mongodb的事务属于乐观事务,不同于MySql悲观事务 ,Mongodb的事务使用的隔离级别为SI(Snapshot Isolation)。
- 1、乐观事务会有冲突检测,检测到冲突则立即
throw Conflict(也就是上面提到的WriteConflict) - 2、乐观事务推崇的是更少的资源锁定时间,达到更高的效率,跟以往我们使用的
MySql事务还是有比较大的区别的 - 3、所以可以理解不支持
MySql那样的行锁-悲观锁
解决思路
方法一:修改事务锁等待超时时间
maxTransactionLockRequestTimeoutMillis参数表示事务锁等待超时时间,即当两个事务发生写冲突的时候,后面的事务等待获取锁的事件,如果前面的事务在这个时间内执行结束并且释放锁,后面的事务就不会抛出WriteConflict写冲突异常,继续执行事务操作。
- 方式一:使用这个可以在线修改这个值
db.adminCommand( { setParameter: 1, maxTransactionLockRequestTimeoutMillis: 36000000} );
- 方式二:启动的时候加入参数
mongod --setParameter maxTransactionLockRequestTimeoutMillis=36000000
- 方式三:在(
/etc/mongod.cnf)中加入一下配置(老版配置文件)
setParameter = maxTransactionLockRequestTimeoutMillis=36000000
- 方式四:在新版yaml配置文件(
/etc/mongod.cnf)中加入一下配置(推荐)
setParameter:
# 事务锁超时最长时间(毫秒)
maxTransactionLockRequestTimeoutMillis: 36000000
方法二:写操作重试
使用spring-data-mongo官方推荐使用spring-retry框架进行重试
- 1.添加依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
- 2.启用
spring-Retry,在启动类上面需要添加@EnableRetry注解
@EnableRetry
public class demoApplication {
public static void main(String[] args) {
SpringApplication.run(demoApplication.class);
}
}
- 3.在
service层事务方法添加@Retryable注解
/**
* @author: huangshuai
* @Description
* @date 2021/12/20 10:21
*/
@Service
public class TestService {
@Autowired
@Qualifier("interactionMongoTemplate")
private MongoTemplate mongoTemplate;
@Retryable(value = UncategorizedMongoDbException.class, exceptionExpression = "#{message.contains('WriteConflict error')}", maxAttempts = 128, backoff = @Backoff(delay = 500))
@Transactional(value = "mongodbTransactionManager",propagation = Propagation.REQUIRED)
public Boolean handleA() {
Query query = new Query(new Criteria().andOperator(Criteria.where("_id").is("57cd751e-2d1b-4a60-aa52-888031c99f2d")));
Update update = new Update();
System.out.println("当前处理线程:" + Thread.currentThread().getName());
update.set("postLevel", Thread.currentThread().getName());
UpdateResult updateResult = mongoTemplate.updateMulti(query, update, "t_employee_check_record");
return updateResult.getModifiedCount() > 0;
}
}
@Retryable中的maxAttempts = 128 参数是最大重试次数,可自行调整,backoff = @Backoff(delay = 500) 重试间隔时间(毫秒) 可自行调整。