在多線程環境中,你們耳熟能詳的是死鎖,活鎖。還有一個容易被忽略的是資源競爭(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