数据库更新防并发错误
2019-01-18 本文已影响0人
翔哥不哭
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)
}