模擬鎖情況無效
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.遇到其他事物方法調用這個方法 會有一致性問題 或者鎖提前釋放問題
不推薦