踩坑|以为是Redis缓存没想到却是Spring事务!

news2024/11/28 12:37:42

前言

  最近碰到了一个Bug,折腾了我好几天。并且这个Bug不是必现的,出现的概率比较低。一开始我以为是旧数据的问题,就让测试重新生成了一下数据,重新测试。由于后面几轮测试均未出现,我也就没太在意。

  可惜好景不长,测试反馈上次的问题又出现了。于是我立马着手排查,根据日志的表现,定位是三方服务出问题了。但是我不是非常确定,于是让测试继续观察。

  然而今天又出现了,这次并不是第三方服务引起的。于是我开始逐行审查代码,进行排查。一开始以为缓存的维护策略不对,导致数据库和redis出现数据不一致的情况。但是经过进一步分析日志,发现问题并不是在Redis而是在Spring事务。

场景介绍

  业务场景如下:用户绑定了设备,需要显示在设备列表内,并且可以查看设备信息。

  当用户绑定了一个设备,我需要在数据库内新增一条绑定记录。然后修改用户的策略,在用户的策略里面加上当前的设备,这样就可以查看设备信息了。

  如果用户再次绑定同一个设备,会将原先的记录解绑,再生成一条新的绑定记录,由于是同一个设备覆盖绑定,则不会去修改用户策略。

  如果在设备端或者手机端,进行解绑操作。则服务端会将绑定记录的状态变为解绑,同时用户策略也会删除当前设备。这样就看不到设备信息了。

代码示例

代码结构

@Slf4j
@Service
public class DeviceUserServiceImpl {

    @Resource
    private DeviceUserServiceImpl self;
    
    /**
     * 绑定操作
     */
    @Transactional(rollbackFor = Exception.class)
    public DeviceBindVo doBind(DeviceBindBo bo, DevicePo device) {
      //  绑定逻辑
    }
    
    
    /**
     * 设备解绑
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void unbind(DeviceUnbindBo bo) {
        DeviceUserPo deviceUser = self.getBindInfo(bo.getDeviceId(), bo.getUserId());
        self.unbind(deviceUser, true);
    }

    /**
     * 设备解绑,公共逻辑
     */
    public void unbind(DeviceUserPo deviceUser, boolean modifyPolicy) {
        // 解绑逻辑
    }
    
    /**
     * 获取绑定记录
     */
    @Override
    @Cacheable(value = RedisPrefixConst.DEVICE_USER, key = "#deviceId")
    public DeviceUserPo get(Long deviceId) {
        // 获取绑定记录
    }
}    

绑定逻辑

@Transactional(rollbackFor = Exception.class)
public DeviceBindVo doBind(DeviceBindBo bo, DevicePo device) {
    // 获取设备绑定信息,判断设备是否被绑定
    DeviceUserPo oldDeviceUser = self.get(bo.getDeviceId());

    boolean modifyPolicy = true;

    if (oldDeviceUser != null) {
        self.unbind(oldDeviceUser);
        modifyPolicy = false;
        log.info("设备绑定->已完覆盖绑定的解绑流程");
    }

    // 新增绑定记录
    DeviceUserPo newDeviceUser = new DeviceUserPo();
    log.info("设备绑定->已生成新的deviceUser数据: deviceUserId={}", newDeviceUser.getDeviceUserId());

    // 删除缓存
    self.deleteCache(bo.getUserId(), bo.getDeviceId());

    // 根据需求,更新策略
    if (modifyPolicy) {
        certService.modifyUserPolicy(bo.getUserId());
        log.info("设备绑定->已更新用户证书策略.");
    }

    // 返回绑定信息
    return new DeviceBindVo();
}

解绑逻辑


/**
 * 设备解绑
 *
 * @param bo 参数
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void unbind(DeviceUnbindBo bo) {
    DeviceUserPo deviceUser = self.getBindInfo(bo.getDeviceId(), bo.getUserId());
    self.unbind(deviceUser, true);
}


public void unbind(DeviceUserPo deviceUser, boolean modifyPolicy) {
    // 解绑
    Assert.isTrue(
            // 执行解绑SQL,
            () -> new BizException(DeviceCodeEnum.DEVICE_NOT_BOUND)
    );
    log.info("设备解绑->已更新数据库deviceUser的状态为unbind: deviceUserId={}", deviceUser.getDeviceUserId());

    // 删除缓存
    self.deleteCache(deviceUser.getUserId(), deviceUser.getDeviceId());

    // 更改策略
    if (modifyPolicy) {
        certService.modifyUserPolicy(deviceUser.getUserId());
        log.info("设备解绑->已更新用户证书策略完成: userId={}", deviceUser.getUserId());
    }
}

获取绑定信息

@Override
@Cacheable(value = RedisPrefixConst.DEVICE_USER, key = "#deviceId")
public DeviceUserPo get(Long deviceId) {
    // 查询SQL
}

问题排查过程

  首先我们的业务场景是:用户绑定了设备,需要显示在设备列表内,并且可以点击查看设备信息。
Bug场景是:设备已经绑定成功了,并且显示在设备列表内,但是无法查看设备信息。

错误结论:第三方服务问题

  为什么会这样认为呢?首先无法查看设备信息,一定是策略有问题导致的。但是我查看了这个用户的策略,是有该设备的访问权限。然后我又查看了第三方服务的文档,说修改用户策略,生效时间可能会有几分钟的延迟。

   我问他们有没有试过等几分钟,再查看设备信息。他们说没有,重新绑定了一下就正常了。所以我就回复他们可能是三方服务策略生效时间延迟导致的。

  其实当时,如果他们过几分钟,再测试查看设备信息,如果能正常查看,那就说明是第三方服务策略生效延迟导致的。但是他们并不知道,策略修改以后,可能会延迟生效,就没有做这个场景的测试。

  由于出现Bug了,他们就尝试复现,重新绑定了设备。但是这个Bug不是必现的,接连好几次都成功了,并没有复现出来。所以他们将出现的异常情况告知了我。于是我就开始排查了,但是在排查过程中我忽略了一个关键点,就是他们为了复现Bug,重新测试绑定流程,并且都成功了。这也为我后面得出这个错误结论埋下了一个伏笔。

  由于我忽略了那个关键点,在排查过程中发现用户是有该设备的策略的。现在回过头来看,发现当时大脑估计是短路。因为他们在复现的过程中并没有出现失败,都是成功的,所以策略里面肯定是该设备的。由于策略里面有该设备,并且第三方服务的文档有提到策略可能会延迟生效,所以就得出了第三方服务有问题的结论。

  但是我对这个结论不是非常确定,所以让他们继续观察。并且跟他们说,如果再次出现不要做任何操作。通知我进行排查。然而今天又出现了经过排查发现是策略缺失。所以就排除是第三方服务出问题引起的了。

真实的原因

  既然排除了是策略未生效的问题,发现是策略缺失了。正常情况下,绑定成功了会修改用户的策略,那么为啥没修改呢?

  通过观察绑定代码发现,不修改用户策略只有一种情况下会产生,就是发现设备已经被绑定了,在进行覆盖绑定就不会修改策略。但是实际情况,设备已经解绑了,再进行绑定。按理来说是获取不到已经绑定的信息的。

@Transactional(rollbackFor = Exception.class)
public DeviceBindVo doBind(DeviceBindBo bo, DevicePo device) {
    // 获取设备绑定信息,判断设备是否被绑定  --> 问题点
    DeviceUserPo oldDeviceUser = self.get(bo.getDeviceId());

    boolean modifyPolicy = true;

    if (oldDeviceUser != null) {
        self.unbind(oldDeviceUser);
        modifyPolicy = false;
        log.info("设备绑定->已完覆盖绑定的解绑流程");
    }

    // 新增绑定记录
    DeviceUserPo newDeviceUser = new DeviceUserPo();
    log.info("设备绑定->已生成新的deviceUser数据: deviceUserId={}", newDeviceUser.getDeviceUserId());

    // 删除缓存
    self.deleteCache(bo.getUserId(), bo.getDeviceId());

    // 根据需求,更新策略
    if (modifyPolicy) {
        certService.modifyUserPolicy(bo.getUserId());
        log.info("设备绑定->已更新用户证书策略.");
    }

    // 返回绑定信息
    return new DeviceBindVo();
}

  那么为什么还能获取到,已经绑定的信息呢?由于get方法是加了缓存的,如果还能获取,也就说明在解绑的时候没有清除缓存。导致在绑定的时候,误以是覆盖绑定,才没有去修改策略,导致问题的出现。

@Override
@Cacheable(value = RedisPrefixConst.DEVICE_USER, key = "#deviceId")
public DeviceUserPo get(Long deviceId) {
    // 查询SQL
}

  通过观察解绑逻辑,发现是先更新数据库,再进行删除缓存。虽然在高并发下,可能在极短时间数据库已经解绑了,但是缓存还没来得及清除,获取到的还是已绑定的状态。

  但是对于我这个场景来说是不可能的出现的。由于从解绑设备,到操作设备进入绑定模式,再进行绑定。整个操作的耗时,缓存早就被清理了。并且通过查看接口日志,也发现缓存缺失是被删除了。那么为什么缓存里面还存有绑定信息呢?

  后来发现是其他线程的会获取调用get()方法,获取绑定信息做逻辑处理。由于解绑时删除了缓存,所以这个时候会从数据库里面查询最新的绑定信息并加载进缓存。按理来说这个时候,查询到的应该是解绑的状态,而不是绑定状态。

  在进行代码审查的,我看到unbind(DeviceUnbindBo bo)上有事务,unbind(DeviceUserPo deviceUser, boolean modifyPolicy)没有事务。并且是由自身的代理对象self调用的。根据Spring的事务传播性来讲,最外层开启了事务,并且通过代理对象调用内部方法,该内部方法也是具有事务的。所以说当unbind方法内的所有逻辑执行完后事务才会提交。

/**
 * 设备解绑
 *
 * @param bo 参数
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void unbind(DeviceUnbindBo bo) {
    DeviceUserPo deviceUser = self.getBindInfo(bo.getDeviceId(), bo.getUserId());
    self.unbind(deviceUser, true);
}

public void unbind(DeviceUserPo deviceUser, boolean modifyPolicy) {
    // 解绑
    Assert.isTrue(
            // 执行解绑SQL,
            () -> new BizException(DeviceCodeEnum.DEVICE_NOT_BOUND)
    );
    log.info("设备解绑->已更新数据库deviceUser的状态为unbind: deviceUserId={}", deviceUser.getDeviceUserId());

    // 删除缓存
    self.deleteCache(deviceUser.getUserId(), deviceUser.getDeviceId());

    // 更改策略
    if (modifyPolicy) {
        certService.modifyUserPolicy(deviceUser.getUserId());
        log.info("设备解绑->已更新用户证书策略完成: userId={}", deviceUser.getUserId());
    }
}   

  到这里基本破案了,bug发生的过程如下:当服务端收到解绑请求时,先更改数据库的绑定状态,然后再删除缓存。在执行修改用户策略的时候,其他的线程来查询绑定信息,由于缓存已经被删除了,所以这个时候需要去数据库内查询最新的绑定信息。但是由于unbind方法具有事务,并且修改用户策略还未执行完,所事务并没有提交。导致查询到的还是旧的绑定信息,并将其写入缓存。

  这也就导致了,在重新绑定的时候,明明已经解绑了,获取到的还是绑定的状态。导致进行覆盖绑定,从而没有修改用户策略,设备绑定成功了,但无法查看设备详情。

解决方法

  解决方法非常简单,把@Transactional去掉即可。由于没有事务只要执行完更新SQL就提交了。所以避免在耗时的操作里加上事物,也就避免了上述问题的产生。

总结

  在实际开发中,我们可能一不小心就掉进了Spring事务的坑里了,所以对于事务我们需要特别小心。对于事务,并不是简单的加个@Transactional注解就行了。而是每加一个@Transactional都要认真思考,否则它可能会给你来点意外的惊喜。

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/607509.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode】260. 只出现一次的数字 III

260. 只出现一次的数字 III(中等) 思路 这道题是136. 只出现一次的数字 的进阶版,需要找出两个仅出现一次的元素。有了上一题的基础,我们很容易就想到要用异或来解决,但是由于这题最终会剩下两个不同的元素&#xff0…

设置和使用DragGAN:搭建非官方的演示版

DragGAN的官方版还没有发布,但是已经有非官方版的实现了,我们看看如何使用。DragGAN不仅让GAN重新回到竞争轨道上,而且为GAN图像处理开辟了新的可能性。正式版本将于本月发布。但是现在已经可以在一个非官方的演示中试用这个新工具了 DragGAN…

数据结构:二叉树(初阶)

朋友们、伙计们,我们又见面了,本期来给大家解读一下二叉树方面的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成! C 语 言 专 栏:C语言:从入门到精通 …

Unix/C/C++进阶--SocketCAN 编程

Unix/C/C进阶--SocketCAN 编程 1 介绍1.1 socketcan 简介1.2 can 发展历程1.3 can总线优点 2 知识点2.1 CAN详解--书籍、网站2.2 CAN详解--CAN与com口介绍2.3 CAN详解--各家CAN分析仪与软件的比较2.4 转载:CAN总线终端电阻2.5 如何破解汽车--CAN协议(can…

3.8 电路布线

博主简介:一个爱打游戏的计算机专业学生博主主页: 夏驰和徐策所属专栏:算法设计与分析 1.最优子结构的证明: 我的理解: 对于电路布线问题的最优子结构性质,我们可以通过数学推导进行证明。下面是对证明的…

conda在 powershell下不能激活虚拟环境

这里写自定义目录标题 问题原因解决办法增加环境变量修改PowerShell 策略初始化conda环境安装或更新conda 结果 问题原因 powershell正常是不行的,但是在cmd中是可以的 问题产生的原因有很多: 必须无法识别activate.bat激活无反应 解决办法 增加环…

【JavaSE】Java基础语法(四十六):枚举

文章目录 1. 概述2. 定义格式3. 枚举的特点4. 枚举的方法 1. 概述 枚举是一种特殊的数据类型,它列出了一组预定义的常量,并使用标识符来引用这些常量。枚举的用途很广泛,下面列举了几个常见的应用场景: 管理常量:如果您…

计算机组成原理---第三章存储系统 习题详解版

(一)精选课内习题 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 (二)精选课后习题 1.设有一个具有20位地址和32位字长的存储器,问: (1)该存储器能存储多少个字节的信息? (2)如果存储器由512k8位的SR…

Linux :: vim 编辑器:详解:文本复制/粘贴/剪切/删除 与 撤销普通操作及撤销撤销操作

前言:本篇是 Linux 基本操作篇章的内容! 笔者使用的环境是基于腾讯云服务器:CentOS 7.6 64bit。 学习集: C 入门到入土!!!学习合集Linux 从命令到网络再到内核!学习合集 前文&#x…

chatgpt赋能python:Python去掉None:提高代码效率,优化SEO

Python去掉None:提高代码效率,优化SEO 作为一名有10年Python编程经验的工程师,我发现Python中会频繁出现None类型的变量。这种情况在代码中一旦过多,就会影响程序的效率,同样也会影响SEO的排名。因此,为提…

【数据仓库架构】什么是 Azure Synapse,它与 Azure Data Bricks 有何不同?

Azure Synapse Analytics 是一项针对大型公司的无限信息分析服务,它被呈现为 Azure SQL 数据仓库 (SQL DW) 的演变,将业务数据存储和宏或大数据分析结合在一起。 在处理、管理和提供数据以满足即时商业智能和数据预测需求时,Synapse 为所有工…

Hive学习---5、文件格式和压缩、企业级调优

1、文件格式和压缩 1.1 Hadoop压缩概述 由于Hive是相当于与Hadoop的客户端,所以hadoop会啥压缩,Hive基本就会啥压缩。 压缩格式算法文件扩展名是否可切分DEFLATEDEFLATE.deflate否GzipDEFLATE.gz否bzip2bzip2.bz2是LZOLZO.lzo是SnappySnappy.snappy否…

word恢复和粘贴按钮变灰色,不可用怎么办?

如果 Word 中的恢复和粘贴按钮变成灰色,可能是由于以下原因之一: 1. 文档处于只读模式。 2. 与 Office 相关的某些组件已损坏或缺失。 3. Word 的文件权限被配置为只读。 以下是一些可能的解决方法: 1. 检查文档是否处于只读模式。 如果是…

随机数发生器设计(三)

随机数发生器设计(三)- 熵估计和健康测试 熵估计健康测试 熵估计 考虑都熵源的多样性,建立一个通用的熵估计模型比较困难。本文采用nist.sp.800-90B推荐的Markov评估。详见 https://doi.org/10.6028/NIST.SP.800-90B。 执行Markov评估时&am…

chatgpt赋能python:用Python向手机发送信息是如何实现的?

用Python向手机发送信息是如何实现的? 在今天的信息时代,随时随地保持联系已经成为生活不可或缺的一部分。随着技术的发展,我们可以使用各种方式发送和接收信息,而使用Python向手机发送短信是其中一种非常方便的方式。 Python的…

I.MX6ull EPIT定时器

一 简介 EPIT定时器是一种增强的周期中断定时器,完成周期性中断定时的功能。 具有以下特点 EPIT定时器是一个32位的定时器 时钟源可选的向下计数器 EPIT 共有 3 个时钟源可选择,ipg_clk、ipg_clk_32k 和 ipg_clk_highfreq 当计数值和比较值相等的时候…

兼顾性能+实时性处理缓冲数据解决方案

我们经常会遇到这样的数据处理应用场景:我们利用一个组件实时收集外部交付给它的数据,并由它转发给一个外部处理程序进行处理。考虑到性能,它会将数据存储在本地缓冲区,等累积到指定的数量后打包发送;考虑到实时性&…

ChatGPT与软件架构(3) - 软件架构提示工程

高效利用ChatGPT辅助研发的关键是在研发生命周期的不同阶段采用对应提示获取有益的帮助。原文: Leveraging Prompt Engineering in Software Architecture with ChatGPT 软件架构开发生命周期转型。 Beth Smith Unsplash 简介 作为解决方案架构师,有必要掌握软件架构…

【分布式架构】资源与事务:可观测性的基本二重性

西格曼:我叫本西格曼。我是Lightstep的联合创始人兼首席执行官。我在这里讨论的是资源和事务,这是可观察性的一个基本的二元性。我职业生涯的大部分时间都在研究可观察性。在我职业生涯之初,我在谷歌工作了九年,致力于谷歌的分布式…

SLAM实战项目(1) — ORB-SLAM2稠密地图重建

目录 1 整体思路 2 功能实现 3 结果运行 (1) TUM数据集下载 (2) associate.py用于RGB和Depth匹配 (3) 运行数据集 4 CMakeLists.txt文件修改 5 完整PointCloudMapping.h和PointCloudMapping.cc 6 报错分析 7 思考扩展 文章参考部分开源代码和报错文章 1 整体思路 利…