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注解,乐观锁也可以正常执行
出处:https://www.cnblogs.com/gne-hwz/
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任