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注解,樂觀鎖也可以正常執行

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM