前语
最近碰到了一个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
都要认真思考,不然它或许会给你来点意外的惊喜。
结尾
如果觉得对你有帮助,能够多多评论,多多点赞哦,也能够到我的主页看看,说不定有你喜爱的文章,也能够随手点个关注哦,谢谢。
我是不一样的科技宅,每天进步一点点,体验不一样的日子。咱们下期见!