1

最近在项目尝试使用Mongodb的事务特性
遇到了一个情况,两个事务执行修改同一个数据的时候会返回下面的错误

org.springframework.data.mongodb.UncategorizedMongoDbException: Command failed with error 112 (WriteConflict): 'WriteConflict'

WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.,

在并发操作同一个文档的时候,有概率会出现上面的这些提示,然后在重试之后就会没有。

原因

Mongodb的事务属于乐观事务,不同于MySql悲观事务
Mongodb的事务使用的隔离级别为SI(Snapshot Isolation,篇外连接)

1、乐观事务会有冲突检测,检测到冲突则立即throw Conflict(也就是上面提到的WriteConflict)
2、乐观事务推崇的是更少的资源锁定时间,达到更高的效率,跟以往我们使用的MySql事务还是有比较大的区别的
3、所以可以理解不支持MySql那样的行锁-悲观锁

对于出现冲突后的处理方案

MongoDb官方推荐在driver层重试,也就是出现WriteConflict异常自动进行重试。

mongo-java-driver

我观察了一下java-driver提供的withTransaction方法是有重试机制的

image.png
源码位置:com.mongodb.client.internal.ClientSessionImpl#withTransaction(com.mongodb.client.TransactionBody<T>, com.mongodb.TransactionOptions)
无奈Springboot事务本身使用的不是withTransaction进行开启事务,而是使用的单独的这些APIimage.png
所以SpringBoot的mongo事务也就没有自动重试。

解决方案

经过查阅资料

spring-data-mongo官方推荐使用spring-retry框架进行重试

添加依赖

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

启用spring-Retry

Application类上面需要添加@EnableRetry注解

方式一 简单使用

    @Retryable(value = UncategorizedMongoDbException.class, exceptionExpression = "#{message.contains('WriteConflict error')}", maxAttempts = 128, backoff = @Backoff(delay = 500))
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class, timeout = 120)
    public void updateScoreRemark(String remark) {
        业务代码
    }

方式二 快捷使用

定义@MongoTransactional注解

import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.lang.annotation.*;

/**
 * Mongo事务注解
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Retryable(value = UncategorizedMongoDbException.class, exceptionExpression = "#{message.contains('WriteConflict error')}", maxAttempts = 128, backoff = @Backoff(delay = 500))
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class, timeout = 120)
public @interface MongoTransactional {

}

使用@MongoTransactional注解

    @MongoTransactional
    public void updateScoreRemark(String remark) {
        业务代码
    }

注意事项

@Retryable

maxAttempts = 128 参数是最大重试次数,可自行调整
backoff = @Backoff(delay = 500) 重试间隔时间(毫秒) 可自行调整

@Transactional

propagation = Propagation.REQUIRES_NEW 是每次创建新的事务(传播级别)

由于mongodb出现WriteConflict 的时候会abort(rollback)事务,所以重试的时候需要生成新的事务
所以推荐在业务入口处增加【重试和事务】的注解,其他地方不要加,避免一个业务方法中出现两个事务。

timeout = 120 是事务超时时间(秒) 虽然目前没测试出来有什么用,还是建议设置一下


岁月安然
27 声望4 粉丝

随遇而安