數據庫資源競爭原理及解決辦法 (附代碼和測試用例)

    資源競爭簡介    

        在多線程環境中,你們耳熟能詳的是死鎖,活鎖。還有一個容易被忽略的是資源競爭(race condition)。這三個在很大程度上是很是類似的,這裏不展開詳述。java

        在開發過程當中,相信有些人遇到過相似的場景:兩個用戶下單成功了,庫存只少了一個。原理其實很簡單,由多個線程對同一個資源進行讀寫引發。好比線程A讀到了未提交的數據,而後繼續操做這個髒數據,最後將結果寫入到了數據庫。git

    事務和鎖    

        咱們很容易想到經過事務隔離機制來對避免髒讀。例如經過Spring 註解。固然這是第一步,申明成事務後,它確實已經按事務運行,但部署以後仍會發現上述問題。web

        這裏須要注意的是,多線程環境下它可能會讀到「舊」值(注意和髒讀區分),例如兩個線程同時進入了這個方法;或者上一個線程未完成提交,新的進程又進入了。畢竟,事務並不提供互斥鎖。到這一步,接下來要作的就簡單明瞭了sql

        經過鎖來控制。首先排除Java內置的鎖,分佈式環境很難控制,粒度很差掌握也影響性能。數據庫提供了不一樣級別的鎖,庫鎖,表鎖,行鎖。在這裏,咱們要用的是行鎖。不少作web開發的可能對行鎖並不太瞭解,具體能夠查閱官方文檔。數據庫

        不一樣數據庫的語法有些不一樣,MySQL中是「select *** for update」。要注意的是行鎖要指定主鍵,不然會退化成表鎖。多線程

        這樣一步步分析下來,第一種解決方法就出來了。事務+行鎖。            dom

public void rowLock() throws SQLException {
        Connection connection  = getDataSource().getConnection();

        boolean autoCommit = connection.getAutoCommit();
        connection.setAutoCommit(false);
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("select * from stock where id = 1 and visible = 1 for update");
        resultSet.next();
        int current_value = resultSet.getInt("current_value");
        statement.execute("update stock set visible = 0 where id = 1");
        /**
         * do other things
         */
        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        statement.execute("update stock set current_value ="+ (current_value-1) +" where id = 1");
        statement.execute("update stock set visible = 1 where id = 1");
        connection.commit();
        connection.setAutoCommit(autoCommit);
    }

        經過設置autocommit爲false開啓事務,使用「select for update」鎖死一行記錄,操做完成後,最後提交。分佈式

版本控制

        除了事務和鎖,還有另外一種解決方案,加入版本號。性能

        簡單來講,咱們的目標是避免讀到過時數據,在這裏,採用的辦法是對操做採用一個版本號,每次操做版本號遞增,若是在更新數據庫的時候該數據的版本號未變化,則說明這一過程沒有另外的線程在干擾。若是版本號已經變化,須要重試。測試

        

public int updateWithVersion(Connection connection) throws SQLException,IllegalStateException {
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("select * from stock where id = 1");
        resultSet.next();
        int version = resultSet.getInt("version");
        int current_value = resultSet.getInt("current_value");

        /**
         * do other things
         */
        try {
            Thread.sleep(new Random().nextInt(100)+100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String sql = "update stock set current_value = %d,version = %d where id = 1 and version = %d";
        sql = String.format(sql,current_value-1,version+1,version);
        int result = statement.executeUpdate(sql);
        return result;

    }

 

 

完整代碼附測試用例https://git.oschina.net/moyiguke/codefamily/blob/master/MysqlRaceCondition.java

相關文章
相關標籤/搜索