事务方法中保证数据只插入一次方案探究

2023-11-15

需求场景

在项目的接口请求中,我们有一个接口A需要事务支持,在接口A中调用了方法B,方法B也需要事务支持,两者都带有@Transactional注解。在B方法中是这个一个逻辑,查询本地数据库是否包含属性值为一个特定值的字段,如果没有的话就插入,如果有的话就跳过。

问题难点及方案分析

首先最简单的方式是在数据层面加入唯一性约束,但是项目中会出现报错,并且这里我们要求不能在数据库层面进行操作,数据的事务的隔离级别必须是可重复读,只能在代码中保证数据插入的数据的唯一性。

@PostMapping("/add")
@Transactional
public String addUser(@RequestBody User user) {
    
    //逻辑代码1...
    //逻辑代码1...
    //逻辑代码1...
    
    //插入逻辑
    boolean andInsertUser = userService.getAndInsertUser(user);
    
    //逻辑代码2...
    //逻辑代码2...
    //逻辑代码2...
    return andInsertUser ? "添加成功" : "插入失败";
}
@Transactional
    public boolean getAndInsertUser(User user) {
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>().eq(User::getAge, user.getAge());
        List<User> list = list(lambdaQueryWrapper);
        if (Objects.nonNull(list) && !list.isEmpty()) {
            return true;
        }
        return save(user);
    }

难点1及方案分析

  • 在并发的情况下可能两次请求查询基本同时执行,都查询到了相同的结果发现没有数据,然后都执行了插入的请求,导致数据的重复。

针对于上述情况,我们可以使用信号量机制来解决,使用信号量之后即使在并发的情况下发生,也只有一个线程能够真正执行里面的内容,并且其他的线程获取资源失败之后并不会阻塞。


@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    Semaphore semaphore = new Semaphore(1);
    @Transactional
    public boolean getAndInsertUser(User user) {
        if (semaphore.tryAcquire()){
            try {
                LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>().eq(User::getAge, user.getAge());
                List<User> list = list(lambdaQueryWrapper);
                if (Objects.nonNull(list) && !list.isEmpty()) {
                    return true;
                }
                return save(user);
            } finally {
                semaphore.release();
            }
        }
        return true;
    }
}

难点2及方案分析

  • 由于可重读读颗粒级别下存在存在幻读问题,我们考虑这样一种情况,两个请求a和b都进入了addUser方法,其中a执行在逻辑代码1的时候,b已经执行完成了插入逻辑,当a执行到插入逻辑的时候semaphore.tryAcquire()一定是可以成功执行的,而由于addUser方法添加了事务注解,这就导致即使b线程已经执行完了插入逻辑但是a在执行插入逻辑的时候,下面的代码在if判断的时候依然查不到刚才b插入的数据,这里是因为a的事务开启是在b插入数据之前,导致a查询的是a开启事务时候的快照,快照中并不存在b刚才插入的数据,这就导致了再次插入数据。
 LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>().eq(User::getAge, user.getAge());
 List<User> list = list(lambdaQueryWrapper);
 if (Objects.nonNull(list) && !list.isEmpty()) {
     return true;
 }
 return save(user);

针对上述的情况,原因就是a事务的开启在b事务提交之前。如果我们两条事务不是并发执行的而是一条事务执行完成之后另一个事务才开启就不会存在这个问题。

  • 一个看似合理的解决方案
private volatile AtomicInteger stamp = new AtomicInteger(0);

@PostMapping("/add")
@Transactional
public String addUser(@RequestBody User user) {
    int stamp_temp = stamp.get();
    //逻辑代码1...
    //逻辑代码1...
    //逻辑代码1...

    //插入逻辑
    boolean andInsertUser = userService.getAndInsertUser(user, stamp_temp, stamp);

    //逻辑代码2...
    //逻辑代码2...
    //逻辑代码2...
    return andInsertUser ? "添加成功" : "插入失败";
}
private static final Semaphore semaphore = new Semaphore(1);
    @Transactional
    public boolean getAndInsertUser(User user, int stamp, AtomicInteger atomicInteger) {
        if (semaphore.tryAcquire()){
            if (Objects.equals(atomicInteger.get(), stamp)) {
                try {
                    LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>().eq(User::getAge, user.getAge());
                    List<User> list = list(lambdaQueryWrapper);
                    if (Objects.nonNull(list) && !list.isEmpty()) {
                        return true;
                    }
                    return save(user);
                } finally {
                    atomicInteger.incrementAndGet();
                    semaphore.release();
                }
            }
        }
        return true;
    }
}

这里采用了类似于解决ABA问题的解决方案,但是会存在这样一种情况,假如两个线程a和b都执行了addUser()方法,假如起始时候stamp的值是0,a线程执行到了逻辑代码2,这个时候stamp一定已经被a线程变成了1,此时,线程b这个时候首先获取stamp的值是1,线程a还没有执行完成,也就是事务还没有提交,后面执行插入逻辑的时候当然一样会存在读取不到最新数据的问题,导致b线程还是能够插入成功。最后两个事务都会提交成功,这样还是会插入两次相同的数据。

  • 使用select for update保证当前读
    这个方法在MySQL中没有进行实验,在postgresql中读已提交和可重复读隔离界别下均读取不到其他事务已经插入但是没有提交的数据。仍然无法解决问题。

难点3及解决分析

以上的解决方案都是在单机环境下的解决方案,多机分布式环境下仍然会存在很多的问题。针对于以上问题,提出一下两种方案:

  • 使用消息中间件将消息发到mq中进行消费,这样就不会和业务前面的业务逻辑代码冗余在一起,提高接口响应速度。但是由于消息消费可能出现并发消费的问题导致同时插入多条相同数据。
  • 使用redis分布式锁来解决消息并发消费的问题,保证分布式环境下,消费消息的同步性。同时可以根据业务逻辑丢弃消息。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

事务方法中保证数据只插入一次方案探究 的相关文章

随机推荐

  • 使用Clang作为编译器 —— Clang 语言扩展

    1 介绍 本文档描述了 Clang 提供的语言扩展 除了这里列出的语言扩展之外 Clang 还旨在支持广泛的 GCC 扩展 有关这些扩展的更多信息 请参阅 GCC手册 2 特性检查宏 语言扩展可能非常有用 但只有在您知道可以依赖它们的情况下
  • 服务管理工具systemctl

    服务管理工具systemctl Linux服务管理两种方式 service 和 systemctl systemd 是Linux系统最新的初始化系统 init 作用是提高系统的启动速度 尽可能启动较少的进程 尽可能更多进程并发启动 syst
  • 单元测试系列之九:Sonar 常用代码规则整理(一)

    更多原创测试技术文章同步更新到微信公众号 三国测 敬请扫码关注个人的微信号 感谢 摘要 公司部署了一套sonar 经过一段时间运行 发现有一些问题出现频率很高 因此有必要将这些问题进行整理总结和分析 避免再次出现类似问题 作者原创技术文章
  • 毕业设计 Spring Boot的网上购物商城系统(含源码+论文)

    文章目录 1 项目简介 2 实现效果 2 1 界面展示 3 设计方案 3 1 概述 3 2 系统开发流程 3 3 系统结构设计 4 项目获取 1 项目简介 Hi 各位同学好呀 这里是M学姐 今天向大家分享一个今年 2022 最新完成的毕业设
  • 学1个月爬虫就月赚6000?别被骗了,老师傅告诉你爬虫的真实情况

    这是我前几天看到的一个真实事件 也是我写这篇文章的缘由 前几天有粉丝跟我反馈说 某机构的人跟他说学爬虫1个月就能接单 让这小伙子去报名那个机构的爬虫课程 学完之后1个月就能把6000多的学费赚回来 可能是因为我和粉丝的交流比较多 所以小伙子
  • RuntimeError: CUDA error: CUBLAS_STATUS_INVALID_VALUE when calling `cublasGemmStridedBatchedExFix

    记录一个问题 由于版本问题导致的 CUBLAS STATUS INVALID VALUE 参考https discuss pytorch org t runtimeerror cuda error cublas status invalid
  • QT技巧 - QT中如何清空layout中所有控件

    delete之前必须setParent NULL 否则程序可能出现意想不到且很难定位的错误 切记 切记 切记 1 前言 layout中的控件可以通过addWidget增加 但是有个问题 增加之后如何删除 并且使其立即生效是一个问题 2 实现
  • 手机端linux练习,Linux认证模拟练习题及答案

    1 设计一个shell程序 在每月第一天备份并压缩 etc目录的所有内容 存放在 root bak目录里 且文件名为如下形式yymmdd etc yy为年 mm为月 dd为日 Shell程序fileback存放在 usr bin目录下 参考
  • c++类模板与友元

    友元 前置声明 在Blob中声明友元所需要的 为了引用模板的一个特定实例 必须首先声明模板自身 template
  • 树莓派RP2040开发板自制24通道 逻辑分析仪

    目录 前言 1 准备工作和前提条件 1 1 Raspberry Pi Pico RP2040板子一个 1 2 Firmware LogicAnalyzer 5 0 0 0 PICO uf2固件 1 3 LogicAnalyzer 5 0 0
  • 墨者学院-身份认证失效漏洞 解题思路

    题目要求 登陆马春生的账号 解题步骤 进去后首先看到提示 test test 登陆后 我们抓个包看看有没有啥信息 开启BP后我们直接在页面右键刷新刷新 好像没啥信息 点一下Forward 有了 20138880322 把末尾改个数字试一试
  • 微信小程序报错 Invalid attempt to destructure non-iterable instance.

    遇到的问题 TypeError Invalid attempt to destructure non iterable instance In order to be iterable non array objects must have
  • 腾讯/阿里/百度 BAT人才体系的职位层级、薪酬、晋升标准

    互联网圈有这么一句话 百度的技术 阿里的运营 腾讯的产品 那么代表互联网三座大山的BAT 内部人才体系有什么区别呢 今天智小培就带领大家看一看 腾讯 1 职级 腾讯职级体系分6级 最低1级 最高6级 同时按照岗位又划分为四大通道 内部也叫
  • 高等数值计算方法学习笔记第4章第二部分【数值积分(数值微分)】

    高等数值计算方法学习笔记第4章第二部分 数值积分 数值微分 四 龙贝格求积公式 第三次课 1 梯形法的递推化 变步长求积法 2 龙贝格算法 五 高斯求积公式 1 一般理论 1定义1例题 2 构造高斯求积公式方法 二 定理加证明 5 Gaus
  • 如何将.sql文件导入到mysql中

    第一 进入mysql数据库 在cmd中输入mysql u root p然后输入password 第二 新建一个数据库 可以与你想要导进来库的名字相同 create database test db 第三 进入所建立的空数据库test db
  • 现在的00后,真是卷死了呀,辞职信已经写好了·····

    都说00后躺平了 但是有一说一 该卷的还是卷 这不 三月份春招我们公司来了个00后 工作没两年 跳槽到我们公司起薪23K 都快接近我了 后来才知道人家是个卷王 从早干到晚就差搬张床到工位睡觉了 最近和他聊了一次天 原来这位小老弟家里条件不太
  • 排序算法(2)

    本文介绍插入排序和希尔排序 插入排序是较为常见的排序算法 希尔排序也是基础的排序算法 废话不多说 具体来看一下两种算法 插入排序 插入排序的基本思想是拿到下一个插入元素 在已经有序的待排数组部分找到自己的位置 然后进行数据的移动 完成该元素
  • Python每日一练第4天——合并两个有序数组

    合并两个有序数组 给你两个有序整数数组 nums1 和 nums2 请你将 nums2 合并到 nums1 中 使 nums1 成为一个有序数组 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 你可以假设 nums1 的空
  • 浅谈深度学习的基础——神经网络算法(科普)

    浅谈深度学习的基础 神经网络算法 科普 神经网络算法是一门重要的机器学习技术 它是目前最为火热的研究方向 深度学习的基础 学习神经网络不仅可以让你掌握一门强大的机器学习方法 同时也可以更好地帮助你理解深度学习技术 人工神经网络早期的研究工作
  • 事务方法中保证数据只插入一次方案探究

    需求场景 在项目的接口请求中 我们有一个接口A需要事务支持 在接口A中调用了方法B 方法B也需要事务支持 两者都带有 Transactional注解 在B方法中是这个一个逻辑 查询本地数据库是否包含属性值为一个特定值的字段 如果没有的话就插