• spring注解@Transactional 和乐观锁,悲观锁并发生成有序编号问题


     

    需求:系统中有一个自增的合同编号,在满足并发情况下,生成的合同编号是自增的。

    测试工具:Apache Jmeter

    实现方法:

    创建一个数据库表。编号最大值记录表

    表结构类似

    CREATE TABLE `project_number_record` (
      `id` varchar(64) NOT NULL,
      `record_year` date DEFAULT NULL COMMENT '记录年份',
      `max_value` int(11) DEFAULT NULL COMMENT '年份最大编号',
      `status` char(1) NOT NULL DEFAULT '0' COMMENT '状态(0正常 1删除 2停用)',
      `create_by` varchar(64) NOT NULL COMMENT '创建者',
      `create_date` datetime NOT NULL COMMENT '创建时间',
      `update_by` varchar(64) NOT NULL COMMENT '更新者',
      `update_date` datetime NOT NULL COMMENT '更新时间',
      `remarks` varchar(500) DEFAULT NULL COMMENT '备注信息',
      `bus_type` varchar(64) DEFAULT '' COMMENT '业务类型(合同,项目)',
      `version` varchar(20) DEFAULT '0' COMMENT '并发数据控制字段,时间戳数值',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='部门项目编号表';

    尝试使用过3种方法进行解决这个问题。

    序号有序尝试方式:
    1、使用@Transaction(readyOnly=false)+synchronized (this){}代码块的方式保证合同编号有序
    2、synchronized (this){} 锁住 调用事务方法的代码
    3、使用乐观锁保证合同编号有序(事务情况下执行需要考虑事务隔离级别问题)

    1、使用@Transaction(readyOnly=false)+synchronized (this){}代码块的方式保证合同编号有序

    遇到一个问题,在事务方法内使用同步代码块  synchronized (this){}

    这种情况下,类代码如下。

    @Transactional(readOnly = false)
        public String generateContractNo(Contract contract) {
            String uniqueOfficeCode="uniqueCode";
            String uniqueOfficeName="uniqueName";
            String numberStr = "0000";
            ProjectNumberRecord projectNumberRecord = new ProjectNumberRecord();
            projectNumberRecord.setOfficeCode(uniqueOfficeCode); //contract.getOfficeCode()
            projectNumberRecord.setBusType(ProjectNumberRecord.BUS_TYPE_CONTRACT);
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(new Date());
            int year = calendar.get(Calendar.YEAR);
            calendar.clear();
            calendar.set(Calendar.YEAR, year);
            projectNumberRecord.setRecordYear(calendar.getTime());//事务和同步锁同时存在导致同步锁失效
            synchronized (this){
                String updateTimeStamp="";
                //获取当前年份的数据记录
                List<ProjectNumberRecord> projectNumberRecordList = projectNumberRecordService.findList(projectNumberRecord);
                ProjectNumberRecord dbProjectNumberRecord = null;
                if (projectNumberRecordList!=null && projectNumberRecordList.size() >= 1) {
                    dbProjectNumberRecord = projectNumberRecordList.get(0);
                } else {
                    //不存在,新增对应的数据
                }
                int maxValue = dbProjectNumberRecord.getMaxValue() + 1;
                dbProjectNumberRecord.setMaxValue(maxValue);
                numberStr = numberStr.substring(String.valueOf(maxValue).length()) + maxValue;
    
                // 在更新数据之前判断是否存在数据
                if(dbProjectNumberRecord.getIsNewRecord()){
                    //新数据
                    projectNumberRecordService.insert(dbProjectNumberRecord);
                }else{
                    // 更新最大值数据
                    dbProjectNumberRecord.setVersion(String.valueOf(System.currentTimeMillis()));
                    long updateStatus = projectNumberRecordDao.updateNumberRecord(dbProjectNumberRecord);
                }
            }
            return numberStr;
        }

    测试结果,10个线程并发产生的同样的合同编号,然后数据库会生成10条相同的数据。结果不符合要求,

     失败原因:

    Synchronized 失效关键原因:是因为**Synchronized**锁定的是当前调用方法对象,而Spring AOP 处理事务会进行生成一个代理对象,并在代理对象执行方法前的事务开启,方法执行完的事务提交,所以说,事务的开启和提交并不是在 Synchronized 锁定的范围内。出现同步锁失效的原因是:当A(线程) 执行完getSn()方法,会进行释放同步锁,去做提交事务,但在A(线程)还没有提交完事务之前,B(线程)进行执行getSn() 方法,执行完毕之后和A(线程)一起提交事务, 这时候就会出现线程安全问题。

    同步锁,锁的是代理对象,锁的对象不同,所以导致同步锁失效。

    实际执行顺序线程是同时执行了。

    A(线程): Spring begins transactional > 方法> Spring commits transactional
    B(线程): Spring begins transactional > 方法> Spring commits transactional
    原文链接:https://blog.csdn.net/prin_at/article/details/90671332

    2、synchronized (this){} 锁住 调用事务方法的代码

     代码如下:

    @RequestMapping(value = "testGenerateContractNo")
        @ResponseBody
        public ReturnObject testGenerateContractNo() {
            Contract contract=new Contract();
            contract.setId("1241525874512580608");
            logger.info("对象哈希编码:"+outSideService.hashCode());
            String contractNo;
            synchronized (contractService){
                contractNo = outSideService.generateContractNo(contract);
            }
    
            return ReturnObject.success(contractNo);
        }

    执行结果:10个线程并发下,生成的合同编号是有序的。可能会存在执行效率慢的问题,因为这是单线程操作。

    3、使用乐观锁保证合同编号有序(事务情况下执行需要考虑事务隔离级别问题)

    @Transactional(readOnly = false)
        public String generateContractNo(Contract contract) {
            String uniqueOfficeCode="uniqueCode";
            String uniqueOfficeName="uniqueName";
            String numberStr = "0000";
            ProjectNumberRecord projectNumberRecord = new ProjectNumberRecord();
            projectNumberRecord.setOfficeCode(uniqueOfficeCode); //contract.getOfficeCode()
            projectNumberRecord.setBusType(ProjectNumberRecord.BUS_TYPE_CONTRACT);
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(new Date());
            int year = calendar.get(Calendar.YEAR);
            calendar.clear();
            calendar.set(Calendar.YEAR, year);
            projectNumberRecord.setRecordYear(calendar.getTime());
            //使用乐观锁,使用更新时间字段来判断数据是否被更新,如果被更新则线程休眠0.2秒
            while(true){
                String updateTimeStamp="";
                //获取当前年份的数据记录
                List<ProjectNumberRecord> projectNumberRecordList = projectNumberRecordService.findList(projectNumberRecord);
                ProjectNumberRecord dbProjectNumberRecord = null;
                if (projectNumberRecordList!=null && projectNumberRecordList.size() >= 1) {
                    dbProjectNumberRecord = projectNumberRecordList.get(0);
                    updateTimeStamp=dbProjectNumberRecord.getVersion();
                    dbProjectNumberRecord.setOldVersion(updateTimeStamp);
                } else {
                    //不存在,新增部门对应的数据
                }
                int maxValue = dbProjectNumberRecord.getMaxValue() + 1;
                dbProjectNumberRecord.setMaxValue(maxValue);
                numberStr = numberStr.substring(String.valueOf(maxValue).length()) + maxValue;
    
                // 在更新数据之前判断是否存在数据
                if(dbProjectNumberRecord.getIsNewRecord()){
                    //新数据
                    projectNumberRecordService.insert(dbProjectNumberRecord);
                    break;
                }else{
                    // 更新最大值数据
                    dbProjectNumberRecord.setVersion(String.valueOf(System.currentTimeMillis()));
                    long updateStatus = projectNumberRecordDao.updateNumberRecord(dbProjectNumberRecord);
    
                    if(updateStatus>0){
                        // 更新成功,没有其他线程更新过数据
                        logger.info("更新成功,没有其他线程更新过数据");
                        break;
                    }else{
                        logger.info("更新失败,休眠1秒");
                        numberStr="0000";
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            return numberStr;
        }

     结果:只有第一个抢占的线程才可以正常获取合同编号,其他9个线程一致在做循环显示更新失败。

    原因是因为,spring事务的隔离级别默认是  Isolation.DEFAULT:为数据源的默认隔离级别。大多数的数据库隔离级别:read committed 读取提交内容,第一个线程的事务更新的这条数据,然后事务还没有提交,导致其他线程读取的version数据不正确,就一直更新失败,死循环。

    当设置数据库隔离级别为:

    @Transactional(readOnly = false,isolation = Isolation.READ_UNCOMMITTED)

    isolation = Isolation.READ_UNCOMMITTED读事务允许其他读事务和写事务,未提交的写事务

    修改完后:结果合同编号有序。

    还有一种方式:去掉@Transactionl注解,乐观锁也可以正常执行

  • 相关阅读:
    动态代理的两种实现方式
    BeanFactory和ApplicationContext的区别
    spring为什么使用三级缓存而不是二级缓存
    updatealternatives简明用法
    在windows10下的命令行终端中vim无法使用鼠标右键粘贴和复制
    在debian11中编译安装vim8.2
    sqlserver使用cpu比较高的语句
    Go 并发编程 — 结构体多字段的原子操作 atomic.Value
    Go单元测试模拟服务请求和接口返回
    关于性能优化技巧
  • 原文地址:https://www.cnblogs.com/gne-hwz/p/12844879.html
Copyright © 2020-2023  润新知