需求:系統中有一個自增的合同編號,在滿足並發情況下,生成的合同編號是自增的。
測試工具: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注解,樂觀鎖也可以正常執行
