数据库更新防并发错误

1. 更新的操作存在的问题

  • 1.1 infomation表每一条诉求记录都是有状态,但是更新记录状态之前需要查询该记录状态,但是一般事务设置的隔离级别,对查询是不会加锁,所以查询出来的状态可能不是最新的,最后更新达不到预想的结果
    @Override
    public Information updateInformationStateCodeForNotice(Long informationId, Long orgId, String departmentNo, Integer stateCode) {
        Information information = getInformationById(informationId);
        // 这里会进行状态逻辑判断
        if(InformationStateCode.INFORMATION_STATECODE_PROCESS.equals(stateCode)
                && stateCode.equals(information.getStateCode())) {
            return information;
        }
        // 这里会进行状态逻辑判断
        if (information.getStateCode() != null && information.getStateCode() > stateCode) {
            throw new BusinessValidationException("无法执行该操作");
        }
        try {
            information.setStateCode(stateCode);
            if (InformationStateCode.INFORMATION_STATECODE_WAIT.equals(stateCode)) {
                information.setCurrentOrgId(orgId);
                information.setCurrentDepartmentNo(departmentNo);
            }
            //这里更新的时候就有可能状态已经变化,如果此时再更新状态就会错乱
            informationMapper.updateInformationStateCodeById(information);
            return information;
        } catch (ServiceValidationException e) {
            throw new ServiceValidationException("更新诉求状态失败!", e);
        }
    }

2. 并发修改方案

A方案 update锁

单条记录查询的时候加上 for update锁,待当前事务提交即释放锁,其他线程才获取锁,这样可以保证查询的结果在当前事务中有效,但是此方法有一定风险,可能会永远锁住某一条记录,也有可能造成死锁,所以不是最优选择。

如:
//普通查询方法
InformationService.getInformationById()
//加X锁查询方法
InformationService.getInformationByIdForUpdate()

x锁sql
如:
getInformationByIdForUpdate:
Select * from informations
WHERE id = #{id} for update

B方案 where带上符合条件的状态

更新的时候where带上符合条件的状态,根据返回结果,是否更新成功,若为更新成功可根据业务是否需要回滚,都可以自行控制,但是此方法只适合查询判断逻辑简单的业务,如需要判断多种条件而且不仅判断当前要更新的记录,还要判断关联数据的逻辑,此方法还是不能保证数据的一致性。

如:通过判断更新返回值,如果没有更新成功,则抛异常回滚。

  • B1代码 :事务内的逻辑,判断要更新的车辆那条记录状态是否能更新
 @Override
    public boolean addApplyCarKeepRecord(CarKeepRecordVO carKeepRecordVO) {
        if (carKeepRecordVO == null) {
            throw new BusinessValidationException("参数不能为空");
        }
        CarKeepRecord carKeepRecord = carKeepRecordVO.getCarKeepRecord();
        if (carKeepRecord == null) {
            throw new BusinessValidationException("参数不能为空");
        }
        CarInfo carInfo = validateCarInfoExsit(carKeepRecord.getCarId());
        try {
            carKeepRecord.setCarId(carInfo.getId());
            carKeepRecord.setCarName(carInfo.getCarName());
            carKeepRecord.setStartDate(new Date());
            addCarKeepRecord(carKeepRecord);
            int updateCarStatusByStatusEnum = carInfoService.updateCarStatusByStatusEnum(carInfo.getId(), EnumCarStatusUpdate.published_to_keeping);
            if (updateCarStatusByStatusEnum == 0) {
                throw new BusinessValidationException(EnumCarStatusUpdate.published_to_keeping.getErrorText());
            }
            return true;
        } catch (BusinessValidationException e) {
            throw new ServiceValidationException(e.getMessage(), e);
        } catch (Exception e) {
            throw new ServiceValidationException("提交维护异常", e);
        }
    }
  • b2代码 更新状态的统一方法,所以更新状态的业务,都调用此方法
@Override
    public int updateCarStatusByStatusEnum(Long carId, EnumCarStatusUpdate carStatusEnum) {
        if (carId == null) {
            throw new BusinessValidationException("参数为空异常");
        }
        CarInfo orderInfo = getCarInfoById(carId);
        if (orderInfo == null) {
            throw new BusinessValidationException("车辆不存在异常");
        }
        if (carStatusEnum == null) {
            throw new BusinessValidationException("车辆更新参数出现异常");
        }
        try {
            CarInfoVO carInfoVO = new CarInfoVO();
            carInfoVO.setCarId(carId);
            carInfoVO.setFromStatus(carStatusEnum.getFrom());
            carInfoVO.setToStatus(carStatusEnum.getTo());
            return carInfoMapper.updateCarStatus(carInfoVO);
        } catch (Exception e) {
            throw new ServiceValidationException("车辆状态更新出错", e);
        }
    }
  • b3代码 更新状态的sql
        UPDATE
            uc_sys_car_info
        SET
            update_date = now(),
            car_status  = #{toStatus}
        WHERE id = #{carId} AND car_status = #{fromStatus} AND is_deleted = 0
C方案 redis 做分布式锁

大部分业务流量并发,redis的毫秒级别算安全,所以应该统一采用redis来做分布式锁,这样也不用锁表,还能保证数据一致性

  • 以下是伪代码,仅展示思路

如:
//普通查询诉求方法
Information getInformationById(Long id)
//加redis锁查询诉求方法
Information getInformationByIdForUpdateWithRedis(Long id)

getInformationByIdForUpdateWithRedis方法实现 如:

//加redis锁查询诉求方法
Information getInformationByIdForUpdateWithRedis(Long id){
    boolean lock = false;
    Int getlockCount = 0
    do {
          If(getlockCount !=0 && getlockCount <6){
            Sleep(1000)//睡一秒
          }
          If(getlockCount >=6){
            //尝试获取锁6次都没有获取到,很大可能该id所属的诉求被锁住了
                //可以抛异常,也可以把key抛出,由调用方去处理
          }
        lock = RedisUtils.getInfomationLock(id)
      }while(!lock);
      Return getInformationById(id);
}
    RedisUtils.class的getInfomationLock方法
    //获取诉求记录锁
    Boolean getInfomationLock(Long id){
        String key = “infomationLock:” +id;
        Int reslut = redisClient.setnx(key,1)
        If(reslut == 1){
        Return true
        }
        return false;
    }

    //释放诉求记录锁
    Void unInfomationLock(Long id){
        String key = “infomationLock:” +id;
        redisClient.del(key,1)
    }

你可能感兴趣的:(数据库更新防并发错误)