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

前语

  最近碰到了一个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都要认真思考,不然它或许会给你来点意外的惊喜。

结尾

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

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