java陷阱之spring事物管理導致鎖無效


模擬鎖情況無效

1.創建一個表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `demo`;
CREATE TABLE `demo` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_name` varchar(20) DEFAULT NULL,
  `stock_number` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_name` (`product_name`)
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8;
BEGIN;
INSERT INTO `demo` VALUES (1, '肥皂', 1000);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

2.創建一個下單扣除的方法防止並發導致超買超賣以及臟讀加鎖

ps 我這里用的redis實現的分布式鎖可以直接替換成synchronized測試

 //事物方法 保證一致性
    @Transactional
    public boolean  deductNumber(Long id,int i){
        //定義鎖 庫存id為id的數據
        RLock[] locks = new RLock[]{redissonClient.getLock(String.valueOf(id))};
        RedissonMultiLock redissonMultiLock = null;
        redissonMultiLock = new RedissonMultiLock(locks);
        boolean getLock = false;
        try {
            if (redissonMultiLock != null) {
                //嘗試獲得鎖
                getLock = redissonMultiLock.tryLock();
                if (!getLock) {
                    return false;//系統繁忙請重試
                }
            }
            RowMapper<Demo> rowMapper = new BeanPropertyRowMapper<Demo>(Demo.class);
            //獲得指定產品的庫存
            Demo demo= jdbcTemplate.queryForObject("select * from demo where id=?",rowMapper,id);
            //判斷庫存是否充足
            if(demo.getStockNumber()<i){
                return false;//庫存不足 剩余庫存demo.getStockNumber()
            }
            //庫存扣除
            demo.setStockNumber(demo.getStockNumber()-i);
            //持久化到數據
            jdbcTemplate.update("update demo set stock_number=? where id=?",demo.getStockNumber(),demo.getId());
        } catch (Exception e) {
              return false;
        } finally {
            //釋放鎖
            if (redissonMultiLock != null && getLock) {
                redissonMultiLock.unlock();
            }
        }
        return true;
    }

這里分為五步  1獲得鎖  2查詢數據判斷庫存是否充足 3.庫存扣除 4.持久化到數據庫  5.釋放鎖

3.測試並發場景

   /**
     * 模擬50個人下單 同時扣除庫存
     */
    @Test
    public void run() {
        int threand = 50;//定義50個線程
        ExecutorService executorService = Executors.newFixedThreadPool(threand);
        List<Future<Integer>> futures = new ArrayList<Future<Integer>>();
        for (int i = 0; i < threand; i++) {
            futures.add(executorService.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int succeedCount = 0;
                    //重復扣除1000次
                    for (int j = 0; j < 1000; j++) {
                        boolean isSuccess = tbDmsBasisCompanyConfigureService.deductNumber(1L, 1);
                        //如果扣除成功+1
                        if (isSuccess) {
                            succeedCount++;
                        }
                    }
                    return succeedCount;
                }
            }));

        }
        int count = 0;
        for (int i = 0; i < futures.size(); i++) {
            try {
                count += futures.get(i).get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        //打印成功的數量
        System.out.println(count);

    }

4.驗證結果

 

 可以發現超賣了 我們庫存1000 但是現在賣出1179  再看我們的庫存

189 數據也是異常的

導致異常的分析

由於我們的事物開啟和關閉是由spring托管的  spring事物管理是根據代理模式實現的 我可以把spring的代理方法簡單看成以下

ps:大致這樣 有空看完源碼再回來補充

public boolean invoke(){
    //開啟事物
    ......
    boolean result= tbDmsBasisCompanyConfigureService.deductNumber(1L, 1);
    //根據事物狀態提交和回滾事物
    ......
    return result
  
}

用戶1  1獲得鎖  2查詢數據判斷庫存是否充足 3.庫存扣除 4.持久化到數據庫  5.釋放鎖  庫存還剩999 (並發情況spring還沒來得及提交事物)

用戶2 因為用戶1釋放了鎖 所以用戶2成功獲得鎖  因為用戶1事物還沒來得及提交 RR(mysql默認)或者RC隔離級別 別的事物是不能讀取到未提交的數據 所以用戶2查詢庫存還是1000 這里臟讀 后面導致超買超賣以及庫存扣除

解決方式1

在外部加鎖

/**
     * 模擬50個人下單 同時扣除庫存
     */
    @Test
    public void run() {
        int threand = 50;//定義50個線程
        ExecutorService executorService = Executors.newFixedThreadPool(threand);
        List<Future<Integer>> futures = new ArrayList<Future<Integer>>();
        for (int i = 0; i < threand; i++) {
            futures.add(executorService.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int succeedCount = 0;
                    //重復扣除1000次
                    for (int j = 0; j < 1000; j++) {
                        //定義鎖 庫存id為1的數據
                        RLock[] locks = new RLock[]{redissonClient.getLock("1")};
                        RedissonMultiLock redissonMultiLock = null;
                        redissonMultiLock = new RedissonMultiLock(locks);
                        boolean getLock = false;
                        try {
                            if (redissonMultiLock != null) {
                                //嘗試獲得鎖
                                getLock = redissonMultiLock.tryLock();
                                if (!getLock) {
                                    continue;
                                }
                            }
                            boolean isSuccess = tbDmsBasisCompanyConfigureService.deductNumber(1L, 1);
                            if (isSuccess) {
                                succeedCount++;
                            }
                        } catch (Exception e) {
                            continue;
                        } finally {
                            if (redissonMultiLock != null && getLock) {
                                redissonMultiLock.unlock();
                            }
                        }
                    }
                    return succeedCount;
                }
            }));

        }
        int count = 0;
        for (int i = 0; i < futures.size(); i++) {
            try {
                count += futures.get(i).get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        //打印成功的數量
        System.out.println(count);

    }

測試結果

可以發現數據正確

這個時候可能有疑惑  不是50個人每個人下單1000嗎 怎么庫存不是0  因為並發情況 鎖互斥 大部分都提示系統繁忙請稍后重試了

解決方式2(不推薦)

手動開啟事物

  //防止全局配置了 所以這里定義sprnig 不托管事物
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public boolean  deductNumber(Long id,int i){
        //定義鎖 庫存id為id的數據
        RLock[] locks = new RLock[]{redissonClient.getLock(String.valueOf(id))};
        RedissonMultiLock redissonMultiLock = null;
        redissonMultiLock = new RedissonMultiLock(locks);
        boolean getLock = false;
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//設置事物傳播行為
        TransactionStatus status = null;
        try {
            if (redissonMultiLock != null) {
                //嘗試獲得鎖
                getLock = redissonMultiLock.tryLock();
                if (!getLock) {
                    return false;//系統繁忙請重試
                }
            }
            //開啟事物  開啟事物一定要提交或者回滾 不然又不可預知的問題 
            status = transactionManager.getTransaction(def);
            RowMapper<Demo> rowMapper = new BeanPropertyRowMapper<Demo>(Demo.class);
            //獲得指定產品的庫存
            Demo demo= jdbcTemplate.queryForObject("select * from demo where id=?",rowMapper,id);
            //判斷庫存是否充足
            if(demo.getStockNumber()<i){
                transactionManager.rollback(status);
                return false;//庫存不足 剩余庫存demo.getStockNumber()
            }
            
            //庫存扣除
            demo.setStockNumber(demo.getStockNumber()-i);
            //持久化到數據
            jdbcTemplate.update("update demo set stock_number=? where id=?",demo.getStockNumber(),demo.getId());
            //提交事務
            transactionManager.commit(status);
        } catch (Exception e) {
              return false;
        } finally {
            //釋放鎖
            if (redissonMultiLock != null && getLock) {
                redissonMultiLock.unlock();
            }
            //保險起見加一個這個代碼 如果事物沒提交回滾 執行回滾 一般都是我們代碼問題
            if(status!=null&&!status.isCompleted()){
                transactionManager.rollback(status);
                return  false;
            }
        }
        return true;
    }

缺點

1.忘記提交或者回滾有不可預知問題 后面會分析

2.遇到其他事物方法調用這個方法 會有一致性問題 或者鎖提前釋放問題 

不推薦


免責聲明!

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



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