使用 JPA 實現樂觀鎖

轉載自:https://www.cnkirito.moe/jpa-OptimisticLock/

敘述

樂觀鎖的概念就不再贅述了,不瞭解的朋友請自行百度谷歌之,今天主要說的是在項目中如何使用樂觀鎖,做成一個小 demo。

解決方案

持久層使用 jpa 時,默認提供了一個註解 @Version 先看看源碼怎麼描述這個註解的

@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Version {
}

簡單來說就是用一個 version 字段來充當樂觀鎖的作用。
先來設計實體類

/**
 * Created by xujingfeng on 2017/1/30.
 */
@Entity
@Table(name = "t_student")
public class Student {

    @Id
    @GenericGenerator(name = "PKUUID", strategy = "uuid2")
    @GeneratedValue(generator = "PKUUID")
    @Column(length = 36)
    private String id;

    @Version
    private int version;

    private String name;

    //getter()...
    //setter()...
}

Dao 層

/**
 * Created by xujingfeng on 2017/1/30.
 */
public interface StudentDao extends JpaRepository<Student,String>{

    @Query("update Student set name=?1 where id=?2")
    @Modifying
    @Transactional
    int updateNameById(String name,String id);
}

Controller 層充當單元測試的作用,通過訪問一個 requestMapping 來觸發我們想要測試的方法。

/**
 * Created by xujingfeng on 2017/1/30.
 */
@Controller
public class StudentController {

    @Autowired
    StudentDao studentDao;

    @RequestMapping("student.html")
    @ResponseBody
    public String student(){
        Student student = new Student();
        student.setName("xujingfeng");
        studentDao.save(student);
        return "student";
    }

    @RequestMapping("testVersion.html")
    @ResponseBody
    public String testVersion() throws InterruptedException {
        Student student = studentDao.findOne("6ed16acc-61df-4a66-add9-d17c88b69755");
        student.setName("xuxuan");
        new Thread(new Runnable() {
            @Override
            public void run() {
                studentDao.findOne("6ed16acc-61df-4a66-add9-d17c88b69755");
                student.setName("xuxuanInThread");
                studentDao.save(student);
            }
        }).start();
        Thread.sleep(1000);
        studentDao.save(student);
        return "testVersion";
    }


    @RequestMapping("updateNameById.html")
    @ResponseBody
    public String updateNameById(){
        studentDao.updateNameById("xuxuan2","6ed16acc-61df-4a66-add9-d17c88b69755");
        return "updateNameById";
    }


}

這裏面三個方法,主要是我們想用來測試的三個注意點。
第一個方法 student.html 我們想看看 springdata 如何對 version 字段進行增長的。就不貼圖了,直接給結論,對於添加了 @Version 的註解,我們不需要手動去控制,每一次 save 操作會在原來的基礎上 +1,如果初始爲 null,則 springdata 自動設置其爲 0。
第二個方法 testVersion.html 是樂觀鎖的核心,當多個線程併發訪問同一行記錄時,添加了 @Version 樂觀鎖之後,程序會進行怎麼樣的控制呢?

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.example.jpa.Student#6ed16acc-61df-4a66-add9-d17c88b69755]

異常信息如上,主線程和新線程獲取了同一行記錄,並且新線程優先提交了事務,版本號一致,修改成功。等到了主線程再想 save 提交事務時,便得到一個版本號不一致的異常,那麼在項目開發中就應該自己捕獲這個異常根據業務內容做對應處理,是重試還是放棄 etc…

第三個方法,updateNameById.html 是想強調一下,@Query 中的 updatedelete 操作是不會觸發 springdata 的相關代理操作的,而是轉化爲原生 sql 的方式,所以在項目中使用時也要注意這點。

總結

樂觀鎖,用在一些敏感業務數據上,而其本身的修飾:樂觀,代表的含義便是相信大多數場景下 version 是一致的。但是從業務角度出發又要保證數據的嚴格一致性,避免髒讀等問題,使用的場景需要斟酌。記得前面一片博文簡單介紹了一下行級鎖的概念,其實本質上和樂觀鎖都是想要再數據庫層面加鎖控制併發,那麼什麼時候該用樂觀鎖,行級鎖,什麼時候得在程序級別加同步鎖,又要根據具體的業務場景去判斷。找到能夠滿足自己項目需求的方案,找到性能和可靠性的平衡點,纔是一個程序員的價值所在。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章