生产环境项目踩坑复盘

  1. 缓存需要等事务结束之后再删除,避免旧数据导致数据库和缓存不一致。

说明:

比如线程A在减少账户的余额(11->10),执行了数据库更新,但是事务还未提交,但把缓存删除了。

线程B从缓存里去获取账户的余额,缓存里没有,于是从数据库里去查询。

由于数据库的隔离级别是读已提交,所以拿不到线程A更新的值(10),读取的是未更新的值(11),并存入缓存。

接下来直到这个缓存(11)失效之前,应用拿到的值都是错的。

也就意味着用户看到的数据也是错误的,用户看到自己还剩11,实际上的剩余只剩10了。

正例:

transactionTemplate.execute(status -> {
    try {
        // 3、操作数据库
        this.handlerDb(account);
    } catch (Exception e) {
        status.setRollbackOnly();
        throw e;
    }
    return null;
});
// 5.事务结束之后再删除缓存,避免缓存拿到错误的数据
this.handlerCache(account);

反例:

transactionTemplate.execute(status -> {
    try {
        // 3、操作数据库
        this.handlerDb(account);
        // 4.缓存被删除之后,事务还未提交,其他线程从数据库里拿到旧数据,更新了缓存,从而导致数据库和缓存不一致
        this.handlerCache(account);
    } catch (Exception e) {
        status.setRollbackOnly();
        throw e;
    }
    return null;
});
  1. 事务的失效场景

2.1 被代理的类没有被Spring所管理

2.2 事务的起点是被this 调用,没有真正的去调用代理类

2.3 方法抛出的异常在方法内被捕获,没有被事务拦截器所拦截

  1. 事务的传播机制

说明:

使用Spring事务的时候需要注意事务的传播,如果使用的是Propagation.REQUIRES_NEW ,事务嵌套的时候,如果两个事务分别对同一个数据进行更新,会出现死锁。

比如事务1更新用户的性别,嵌套事务2更新用户的余额,双方都在等待对方释放锁后提交事务,从而导致死锁。

如果需要不同的事务传播机制,可以使用两个bean,设置不同的事务隔离级别。

正例:

@SpringBootConfiguration
public class TransactionConfig {

    @Bean
    public TransactionTemplate transactionTemplate(DataSourceTransactionManager dataSourceTransactionManager) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(dataSourceTransactionManager);
        // 可以设置事务传播方式
   transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        return transactionTemplate;
    }

    @Bean
    public TransactionTemplate newTransactionTemplate(DataSourceTransactionManager dataSourceTransactionManager) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(dataSourceTransactionManager);
        // 可以设置事务传播方式
     transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        return transactionTemplate;
    }
}

  1. 定时任务加分布式锁

说明:

使用定时任务执行任务的时候要加分布式锁,避免这个定时任务还未执行完成,又被下一个定时任务抢到同一个任务导致重复执行

  1. 异常信息捕获打印,打印的时候加上相关信息

说明:

捕获异常的时候需要打印异常堆栈、所处位置、对应的业务标识

正例:

return transactionTemplate.execute(status -> {
    try {
         // ...
    } catch (Exception e) {
        log.error("待处理订单超时或失败操作事务回滚 ,提现id是 {}, 异常信息是 ", withdrawApply.getId(), e); 
        // ...
        return false;
    }
    return true;
});
 

反例:

return transactionTemplate.execute(status -> {
    try {
         // ...
    } catch (Exception e) {
         // ...
        return false;
    }
    return true;
});

  1. 代码里不写死任何测试数据,只能通过配置文件来判断

说明:

测试环境为了方便测试,往往都有一些写死的测试代码,稍有不慎可能就会合并到正式环境,造成严重的损失。

比如在测试环境写死短信验证码为123456,合并到正式环境的时候,如果检查的时候遗漏了,用户可以通过123456登录任何一个账号,操作对方的数据。

为了避免这种情况的发生,不应当在代码里写死任何的测试数据,测试和正式环境的代码不应当有任何区别,区别只能在配置文件上,这样比对代码的时候,每次就只需要比对配置文件,而不需要关注代码。

同时测试数据不能是随意可以猜出来的值,比如123456

正例:

-application-dev.yml

  # 简易验证码配置
  simpleCodeConfig:
    # 是否开启
    enable: true
    # 验证码
    code: 255213

- LoginFacade.java

boolean simpleCodeEquals = mallConfig.getSimpleCodeConfig().getEnable() && Objects.equals(mallConfig.getSimpleCodeConfig().getCode(), loginVO.getCode());
if (!codeEquals && !simpleCodeEquals) {
     // ...
}

反例:

- LoginFacade.java


if (Objects.equals(loginVO.getCode(), "123456")) {
     // ...
}

  1. 不使用BeanUtils 进行拷贝,可以装GenerateAllSetter 和 Alibaba Cloud AI Coding Assistant。

说明:

因为代码里有很多vo、po、bo的转换,所以为了方便,很多时候会使用BeanUtils进行拷贝。

但最好不要用这个,BeanUtils的拷贝可能会带来意想不到的后果,比如本来只是想把新对象修改的值拷贝过来,但却把新对象的null值也拷过来了,导致数据丢失。

或者BeanUtils拷贝的时候,新对象和旧对象的值位置传反,导致数据未能拷贝过来。

推荐添加 GenerateAllSetter idea插件,可以生成这个对象对应的get和set方法,再加上 Alibaba Cloud AI Coding Assistant 的补全功能,可以轻松完成两个对象之间的转换。

正例:

  1. 预生成订单号,避免重复付款,订单号需要唯一索引。

说明:

在涉及到支付、付款、退款等逻辑的时候,订单号需要在创建该笔订单的时候生成,而不是等到真实操作的时候生成,避免真实操作的时候,保存订单号失败,导致一笔订单拥有两个订单号,从而重复退款或者重复付款。

正例:

比如用户中奖,获得了微信红包,那么在保存这条中奖记录之前,就需要生成对应的支付订单号,同步保存下来。真实付款的时候,拿这个订单号去付款。

反例:

保存中奖记录的时候没有保存订单号,付款的时候临时生成订单号这种情况,假如第一次付款由于代码的bug导致付款成功,但订单号等记录未保存下来,那么再次支付的时候会生成新的订单号,导致重复付款,造成资金损失。

  1. 分页order by 时,所排序的字段不能是相同值。

说明:

MySQL order by 语句里,如果有相同的值,MySQL会进行随机排序。如果同时进行了分页,那么上一页和下一页可能会出现重复的值。所以需要在排序的值里加上一个唯一的值,来避免重复数据的出现。

比如大多数时候都需要order by create_time ,但是在并发或者批量导入数据的情况下,很多数据的create_time可能相同,这时候分页就会出现重复值。为了避免这种情况,需要再加上 order by 主键/唯一索引的值 的逻辑。

正例:

select name from user order by create_time desc, id desc limit 10

反例:

select name from user order by create_time desc limit 10

  1. 更新数据库的时候,新建一个对象

说明:

很多时候为了更新方便,会使用全量更新的方式,但这样很容易覆盖其他更新的值。除非特殊情况,或者有十足的把握,否则更新对象的值时,需要使用新建一个对象的方式去更新

正例:

如果要更新status=2

UserInfo user = this.getById(1);
UserInfo updateUser = new UserInfo();
updateUser.setId(user.getId());
updateUser.setStatus(2);
this.updateById(updateUser);

反例:

UserInfo user = this.getById(1);
user.setStatus(2);
this.updateById(updateUser);

比如拿到的这个对象user数据是 id=1,status=1,score=1,此时更新语句是;

update user_info set status =2, score = 1 where id = 2.

如果同时有另外一个线程,把score已经更新为2,然后这条语句才执行。socre就会变回1,这个线程的更新就丢失了。

就可能会出现意料之外的问题。

  1. 线上项目增加字段时不要给字段设置非null限制

说明:

避免功能上线前,旧代码对表进行新增操作时因为非空限制导致接口不可用;如果在增加字段是添加了默认值的情况下可以添加非空限制,需要注意的是这个默认值是否适用旧数据;

对于数字类型比如:int、bigint、decimal、double增加非nul限制的情况下,会自动填充0;

对于字符串类型比如:varchar增加非null限制的情况下,会自动填充空字符串;

如果新增了一个用于表示状态的字段,其中0是其中一个状态,如果旧数据被自动填充的0不应该是初始值,会造成数据混乱;

  1. 修改线上项目已有接口要兼容未修改前的情况

说明:

前端通常会有缓存,需要刷新后才能显示前端最新的页面,如果接口入参增加并且增加了非空限制,会导致有缓存的用户调用这个接口总是报错;如果兼容之前的情况会导致业务出现问题可以不兼容;如果是查询展示的接口通常都需要兼容;

  1. 调用第三方接口打印调用时长

说明:

调用第三方接口在方法结束后打印耗时,在出问题后可以排查是否是因为第三方的问题导致的;

第三方接口执行时间过长导致大部分线程都在等待第三方返回,导致服务卡顿或不可用时可以通过日志排查问题;

  1. 使用JacksonUtil 或者 Gson 进行反序列化时,反序列化的对象结构层级不能太深,否则会导致OOM

说明:

出现一个接口调用后程序OOM,排查后发现因为方法入参有HttpServletRequest,编写的全局拦截器在异常时会进行方法入参的反序列化,打印入参。由于HttpServletRequest 这个对象里面的方法和属性较多,反序列化时占用了近3G内存,导致程序OOM。

这是因为Gson 解析对象的时候,需要解析对象的属性和属性名称,如果对象层级较深,解析时间会非常的长,而且解析后的结果对象因为尚未完成,不会被垃圾回收,所以会一直占用内存。

猜你喜欢

转载自blog.csdn.net/socct_yj/article/details/128969516