数据更新应注意问题


场景

在日常开发中,我们经常需要编写 数据更新业务(下面引文中是我自己对 数据更新 的定义),其核心逻辑如下:

  1. 数据校验,如 库存是否有剩余,队伍人数是否已达到上限
  2. 更新数据

数据更新 分为 修改数据新增记录

修改数据:直接修改表中代表数据量的字段,如 商品库存减1

新增记录:一般是多对多的关系表,通过统计表中记录数来表示某种意义的数据,如 用户队伍关系表,通过队伍 id 统计出某一队伍的人数;新增记录,记录的统计数据更新,对应具体意义数据也更新,如 用户队伍关系表新增一条数据,某一队伍人数加1

在编写数据更新业务时,需要注意以下2个问题:

  1. 多线程安全问题
  2. 事务

多线程安全问题

校验数据、更新数据 是先后执行,不是作为原子操作,当多线程并发访问时就可能发生多线程安全问题,如超卖问题。

并发问题有以下2种解决方法:悲观锁乐观锁

悲观锁

悲观锁 认为多线程安全问题一定会发生,因此需要先获取锁,再执行数据更新业务,确保线程串行执行。

将悲观锁分为两类:单机锁、分布式锁

单机锁

单机锁 只对单个 JVM 的线程有效,如果项目部署在单台机器上,可以使用单机锁

常用的单机锁有 synchronized;

synchronized 使用注意:锁对象必须是同一对象。常用的技巧就是 字符串调用 intern() 方法返回常量池中同一个 String 引用,确保同一个对象,示例如下:

// 加锁粒度是同一个用户的多个线程
Long userId = ...... // 用户id
// 加intern()返回的是同一个对象引用
// 不加intern()每次都会new一个String
synchronized(userId.toString().intern()) {
    
}

分布式锁

如果项目是集群部署,可以使用分布式锁

常用的分布式锁有 Redisson 的 RLock。

加锁注意

  • 加锁范围:要覆盖 校验数据、更新数据 两部分加锁范围尽量小

    如 扣减库存业务,先判断库存是否有剩余,再扣减库存;加锁范围是判断+扣减;如果加锁范围只是判断,还是有线程安全问题。

  • 锁的粒度将锁的粒度尽量控制小些,这样性能才好;因为锁的粒度越大,串行执行的线程越多,性能越差。

乐观锁

乐观锁 认为线程安全问题不一定发生,因此不加锁,只是在修改数据时判断是否有其他线程对数据做了修改。如果没有修改,则说明数据是安全的,可以进行修改。

使用乐观锁注意只适用于 修改数据,不可用于新增记录。

乐观锁的两种实现方法:

  • 版本号法:给数据增加一个版本号字段,每次修改操作版本号 + 1,就可以通过版本号来判断数据是否有被修改。

  • CAS:是对乐观锁的简化,即直接用一个每次都会查询和修改的字段来代替版本号,下面用扣减库存来演示CAS

    // 先查询库存
    int oldStock = getStock();
    // 再以oldStock为条件判断库存是否修改
    update tb set `stock` = `stock` - 1 where `stock` = oldStock and 查询条件;
    

    上面这种方式的缺点是成功的概率太低,因此改为使用下面的方法。

    只要库存有剩余,即 stock > 0,就可以扣减库存,因此只要在修改时加一条件:stock > 0 即可

    update tb set `stock` = `stock` - 1 where `stock` > 0 and 查询条件;
    

事务

一段业务逻辑中,如果需要进行 2次以上的 增删改 操作,就要考虑是否将这些操作作为一个事务处理。

Spring 的 @Transactional 可以非常方便地实现事务(声明式事务),@Transactional 有以下注意事项:

@Transactional 注意事项详细见文章:@Transactional

  1. 每次 增删改 操作后,要判断当前操作是否成功如果失败,手动抛出自定义业务异常,让事务回滚,还原之前的操作(@Transactional(rollbackFor = Exception.class) 抛出指定的异常类型,事务回滚)。

  2. 最小粒度是作用在方法上。如果想要给一部分代码块增加事务的话,那就需要把这个部分代码块单独抽取出来作为一个方法。

  3. @Transactional 标注的方法应是 public 修饰

  4. 同一个类中方法调用,会导致 @Transactional 失效;正确的做法应是 不要直接调用,而是 先获取代理对象,再通过代理对象调用方法

  5. 在事务方法内部 try…catch 捕获异常 导致事务失效

    但是,如果是在事务方法外部 try…catch 并不会导致事务失效

事务方法内部加锁的问题

@Transactional 标注的事务方法,在方法内部加锁(悲观锁),可能会导致问题:当前方法事务还没有提交,但是锁已经释放;这样后面的线程拿到锁后,由于前面线程的事务还未提交,数据库的数据未更新,所以后面线程数据校验就会出错,造成多线程安全问题。

因此,应该将锁的范围覆盖整个事务方法,这样就能确保是先提交的事务,再释放锁

示例:

事务方法内部加锁

@Transactional
public void txMethod() {
	synchronized(xxx){
         
    }
}

锁的范围覆盖整个事务方法:

// 事务方法
@Transactional
public void txMethod() {

}

// synchronized代码块内调用txMethod
public void test() {
	synchronized(xxx){
         通过代理对象调用txMethod
    }
}

如果有帮助的话,可以点个赞支持一下嘛🙏

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值