一、前言
1. 事務4個特性
1.原子性(atomic),事務必須是原子工作單元;對於其數據修改,要麼全都執行,要麼全都不執行
2.一致性(consistent),事務在完成時,必須使所有的數據都保持一致狀態。
3.隔離性(insulation),由併發事務所作的修改必須與任何其它併發事務所作的修改隔離。
4.持久性(Duration),事務完成之後,它對於系統的影響是永久性的。
2. 事務併發
通常為了獲得更好的運行性能,各種數據庫都允許多個事務同時運行,這就是事務併發。3. 事務隔離機制
當併發的事務訪問或修改數據庫中相同的數據時,通常需要採取必要的隔離機制。
解決併發問題的途徑是什麼?答案是:採取有效的隔離機制。
怎樣實現事務的隔離呢?隔離機制的實現必須使用鎖
4. 事務併發問題
1.第一類丟失更新:(在秒殺場景會出現問題)
當事務A和事務B同時修改某行的值,
1.事務A將數值改為1並提交
2.事務B將數值改為2並提交。這時數據的值為2,事務A所做的更新將會丟失。
解決辦法:對行加鎖,只允許併發一個更新事務。(hibernate中的悲觀鎖,樂觀鎖)
2.髒讀
1.李四的原工資為8000, 財務人員將李四的工資改為了10000(但未提交事務)
2.李四讀取自己的工資 ,發現自己的工資變為了10000,歡天喜地!(在緩存中讀取)
3.而財務發現操作有誤,回滾了事務,張三的工資又變為了8000 像這樣,張三記取的工資數10000是一個髒數據。
解決辦法:如果在第一個事務提交前,任何其他事務不可讀取其修改過的值,則可以避免該問題。
3.虛讀
目前工資為5000的員工有20人。
1.事務1,讀取所有工資為5000的員工。
2.這時事務2向employee表插入了一條員工記錄,工資也為5000
3.事務1再次讀取所有工資為5000的員工共讀取到了11條記錄,
解決辦法:如果在操作事務完成數據處理之前,任何其他事務都不可以添加新數據,則可避免該問題。
4.不可重複讀
在一個事務中前後兩次讀取的結果並不致,導致了不可重複讀。
1.在事務1中,Mary 讀取了自己的工資為1000,操作並沒有完成
2.在事務2中,這時財務人員修改了Mary的工資為2000,並提交了事務.
3.在事務1中,Mary 再次讀取自己的工資時,工資變為了2000
解決辦法:如果只有在修改事務完全提交之後才可以讀取數據,則可以避免該問題。
5.第二類丟失更新
多個事務同時讀取相同數據,並完成各自的事務提交,導致最後一個事務提交會覆蓋前面所有事務對數據的改變
5. 悲觀鎖樂觀鎖介紹
1. 悲觀鎖
如果使用了悲觀鎖(加了一個行鎖),如果事務沒有被釋放,就會造成其他事務處於等待,所以這裡不使用悲觀鎖。
使用數據庫提供的鎖機制實現悲觀鎖。
如果數據庫不支持設置的鎖機制,hibernate會使用該數據庫提供的合適的鎖機制來完成,而不會報錯。
使用session.load(class,id,LockOptions);加悲觀鎖,相當於發送SELECT ... FOR UPDATE
使用session.get(class,id,LockOptions);加悲觀鎖,相當於發送SELECT ... FOR UPDATE
使用session.buildLockRequest(LockOptions).lock(entity);加悲觀鎖,相當於發送SELECT id
FROM ... FOR UPDATE
使用query.setLockOptions(LockOptions);加悲觀鎖,相當於發送SELECT... FRO UPDATE
2.樂觀鎖
推薦使用 version方式;
version方式:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。
當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛才讀取到的version值為當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功
二、代碼測試
1. 控制庫存不能為負
//控制不能賣出負數的產品mysql不支持check約束,不能通過數據庫控制控制負數 public void setNum(Integer num) { if (num < 0) { throw new RuntimeException("庫存不夠,請刷新再購買"); } t his.num = num; }
2.沒有加樂觀鎖
1.多事務順序執行。假設總庫存10件
//事務1初始庫存10件,賣出去5件,最終庫存5件 //事務2賣出8件最終庫存‐3件,庫存為負數。 @Test public void update() throws Exception { // 事務1 Session session = HibernateUtils.getSession(); session.beginTransaction(); Product product = (Product) session.get(Product.class, 1L); product.setNumber(product.getNumber() ‐ 5); session.update(product); session.getTransaction().commit(); session.close(); // 事務2 Session session2 = HibernateUtils.getSession(); session2.beginTransaction(); Product product2 = (Product) session2.get(Product.class, 1L); product2.setNumber(product2.getNumber() ‐ 8); session2.update(product2); session2.getTransaction().commit(); session2.close(); }
2.多事務交叉執行。
//事務1初始庫存10件,賣出去8件,最終庫存2件,還沒有提交。 //事務2賣出5件最終庫存5件,庫存為5提交事務後庫存為5。 //賣出10件庫存賣出13件,剩餘庫存5件。 @Test public void update2() throws Exception { Session session = HibernateUtils.getSession(); Session session2 = HibernateUtils.getSession(); session.beginTransaction(); session2.beginTransaction(); Product product2 = (Product) session2.get(Product.class, 1L); Product product = (Product) session.get(Product.class, 1L);// 庫存都是10 product2.setNumber(product2.getNumber() ‐ 8);// 10‐8=2 product.setNumber(product.getNumber() ‐ 5);// 10‐5=5 session.update(product); session2.update(product2); session2.getTransaction().commit();// 更新為2 session.getTransaction().commit();// 更新為5 session2.close(); session.close(); }
3.從上面看出,沒有加樂觀鎖。無論事務順序執行還是交叉執行,庫存都會出問題。
3. 加了樂觀鎖
準備工作
//添加一個字段Integer version,不由程序員維護,由hibernate自己維護 private Integer version; 映射文件
實體類代碼
public class Product { private Long id; private String name; private Integer num; // 添加一個字段Integer version,不由程序員維護,由hibernate自己維護 private Integer version; public Long getId() { return id; } p ublic void setId(Long id) { this.id = id; } p ublic String getName() { return name; } p ublic void setName(String name) { this.name = name; } p ublic Integer getNum() { return num; } p ublic void setNum(Integer num) { if (num < 0) { throw new RuntimeException("庫存不夠,請刷新再購買"); } t his.num = num; } @ Override public String toString() { return "Product [id=" + id + ", name=" + name + ", num=" + num + "]"; } }
映射文件代碼
測試代碼
//假設庫存為1。 //事務操作流程:先查詢,獲取庫存,在購買,再更新 //事務1查詢select o from table where id=100 num=10 version=0 //事務2查詢select o from table where id=100 num=10 version=0 //事務1查詢購買8件 //事務2查詢購買5件 //事務2更新,提交 庫存5件 //Update xxx set version=version+1 num=? //Where id=100 and version=0 //事務1更新,報錯,拋出異常 //Update xxx set version=version+1 num=? //Where id=100 and version=0 因為version已經改變 //初始庫存10件,賣出去5件,最終庫存5件 //如果出現樂觀鎖異常,就捕獲StaleObjectStateException異常 //功能實現。 @Test public void update3() throws Exception { try { Session session = HibernateUtils.getSession(); Session session2 = HibernateUtils.getSession(); session.beginTransaction(); session2.beginTransaction(); Product product2 = (Product) session2.get(Product.class, 1L);// 庫存都是1 version=0 Product product = (Product) session.get(Product.class, 1L);// 庫存都是1 version=0 product2.setNumber(product2.getNumber() ‐ 1);// 1‐1=0 product.setNumber(product.getNumber() ‐ 1);// 1‐1=0 session.update(product); session2.update(product2); // update Product set version=1, name=?, price=?, number=0 where id=1 and version=0 // 數據庫最新是version=1 session2.getTransaction().commit();// 更新為2 // update Product set version=1, name=?, price=?, number=0 where id=1 and version=0 // version=0 數據庫最新是version=1 session.getTransaction().commit();// 異常 session2.close(); session.close(); } catch (StaleObjectStateException e) {// 庫存已經更新 Session session3 = HibernateUtils.getSession(); Product product3 = (Product) session3.get(Product.class, 1L); System.out.println("庫存已經更新,最新庫存為:" + product3.getNumber()); session3.close(); }
4. 總結:
以上是hibernate框架使用樂觀鎖的version方式處理庫存不為負的代碼測試。謝謝觀看。
閱讀更多 黑馬程序員成都中心 的文章