@Transactional、synchronized、Mysql的update语句一起使用可能造成表死锁

错误代码

@Component
@Slf4j
public class PropertyNameStatManagerImpl implements PropertyNameStatManager {
    
    

	// 批量保存
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void batchSavePropertyStat(List<PropertyStatDto> dtos) {
    
    
        ……

        // 批量保存属性值和属性名称
        for (PropertyStatDto dto : dtoList) {
    
    
        	// 位置1
            this.save(dto);
        }
    }

	// 批量删除
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void batchRemovePropertyStat(List<PropertyStatDto> dtos) {
    
    
        ……

        // 批量删除属性值和属性名称
        for (PropertyStatDto dto : dtoList) {
    
    
        	// 位置2
            this.remove(dto);
            propertyValueStatManager.remove(dto);
        }
    }

	// 单个保存
    @Override
    public synchronized void save(PropertyStatDto dto) {
    
    
        saveOrRemove(dto, 1);
    }

	// 单个删除
    @Override
    public synchronized void remove(PropertyStatDto dto) {
    
    
        saveOrRemove(dto, -1);
    }

    /**
     * 保存 / 删除 统计信息
     *
     * @param dto       统计信息
     * @param changeNum 改变数量
     * @author 明快de玄米61
     * @date 2022/11/21 12:06
     **/
    private void saveOrRemove(PropertyStatDto dto, Integer changeNum) {
    
    
        // 获取最新统计结果
        PropertyNameStat stat = null;
        List<PropertyNameStat> stats = service.list(Wrappers.<PropertyNameStat>lambdaQuery().eq(PropertyNameStat::getPropertyId, dto.getPropertyId()));
        if (stats.isEmpty()) {
    
    
            PropertyNameStat entity = insertPropertyNameStat(dto.getPropertyId(), dto.getPropertyName());
            if (entity != null) {
    
    
                stat = entity;
            }
        } else {
    
    
            stat = stats.get(0);
        }

        // 判断统计结果
        if (stat == null) {
    
    
            String ops = changeNum > 0 ? "添加" : "删除";
            log.error("》》》%s属性名称统计结果失败,参数:%s", ops, JSON.toJSONString(dto));
            return;
        }

		// 位置3
        // 更新统计结果
        UpdateWrapper<PropertyNameStat> wrapper = new UpdateWrapper<>();
        wrapper.eq(PropertyNameStat.PROPERTY_ID, dto.getPropertyId());
        String operator = changeNum > 0 ? "+" : "";
        wrapper.setSql(PropertyNameStat.NUM + "=" + PropertyNameStat.NUM + operator + changeNum);
        service.update(null, wrapper);
    }

    /**
     * 保存属性名称统计对象
     *
     * @param propertyId   属性id
     * @param propertyName 属性名称
     * @return 属性名称统计对象
     * @author 明快de玄米61
     * @date 2022/11/21 14:05
     **/
    private PropertyNameStat insertPropertyNameStat(String propertyId, String propertyName) {
    
    
        // 初始化
        PropertyNameStat entity = new PropertyNameStat();
        entity.setPropertyId(propertyId);
        entity.setPropertyName(propertyName);
        entity.setDataEnable(EnableStatusEnum.ENABLE.getCode());
        entity.setDataDelete(DeleteStatusEnum.NOT_DELETE.getCode());
        entity.setNum(0L);

        // 添加
        boolean save = service.save(entity);

        return save ? entity : null;
    }
}

分析

如果用户A执行batchSavePropertyStat()方法,然后执行了位置1的this.save()方法,由于save()方法里面有saveOrRemove()方法,该方法中我用了update语句,由于我暂时没有为update语句中where后面的字段添加索引,这样就会触发锁表,详情请看:数据库update语句到底是行锁还是表锁?,由于事务还没有结束,然后this.save()结束,准备在位置1在执行this.save()

此时用户B执行batchSavePropertyStat()方法,然后执行了位置1的this.save()方法,由于save()方法有synchronized锁,那么用户A就要等待,此时用户B去执行this.save()方法,,由于save()方法里面有saveOrRemove()方法,该方法中我用了update语句,由于我暂时没有为update语句中where后面的字段添加索引,所以update方法执行会先获取表锁,但是此时用户A控制的事务没有提交,这样就形成了表死锁

猜你喜欢

转载自blog.csdn.net/qq_42449963/article/details/130283187