MySql悲觀鎖和樂觀鎖總結
現在我有一個購買商品的需求,我們知道當我們購買商品時,後臺會進行減庫存和增加購買記錄的操作。我們分別在無鎖和樂觀鎖和悲觀鎖進行相應的程式碼演示來說明問題。
建表語句如下:
CREATE TABLE `stock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名稱', `count` int(11) NOT NULL COMMENT '庫存', `sale` int(11) NOT NULL COMMENT '已售', `version` int(11) NOT NULL COMMENT '樂觀鎖,版本號', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 CREATE TABLE `stock_order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `sid` int(11) NOT NULL COMMENT '庫存ID', `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名稱', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '建立時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=981 DEFAULT CHARSET=utf8
一、無鎖的Mysql:
先看程式碼:
public class Test2 { public static void main(String[] args) throws SQLException, InterruptedException { Test2 tests = new Test2(); Thread[] threads = new Thread[100]; for (int i=0;i<100;i++){ threads[i] = new Thread(){ @Override public void run() { try { tests.service(); }catch (Exception e){ e.printStackTrace(); } } }; } for (int i=0;i<100;i++){ threads[i].start(); } } public void service() throws Exception { Connection connection = dbUtils.getConnection(); String selectSql = "select count from stock where id = 1"; PreparedStatement statement1 = connection.prepareStatement(selectSql); ResultSet resultSet = statement1.executeQuery(); resultSet.next(); String count = resultSet.getString("count"); System.out.println(count); int c = Integer.parseInt(count); Thread.sleep(10); if (c<1) throw new Exception(); String updateSql = "update stock set count = count - 1 where count > 0"; PreparedStatement preparedStatement = connection.prepareStatement(updateSql); int update = preparedStatement.executeUpdate(); String insertSql = "insert into stock_order(sid,name) VALUES (1,'aaa')"; PreparedStatement statement = connection.prepareStatement(insertSql); int insert = statement.executeUpdate(); } }
從上述程式碼可以看到,有一百個執行緒去模擬一百個使用者購買商品,資料庫中只有10個商品,所以當商品賣完時,應該增加10條購買記錄。為了讓大家看個清楚,我在程式碼中加入了執行緒的睡眠。
我們看到,增加了11條記錄,也就是所謂的超賣現象,商家絕不可能允許這種情況的發生。
MySql的樂觀鎖:
我們在使用樂觀鎖時會假設在極大多數情況下不會形成衝突,只有在資料提交的時候,才會對資料是否產生衝突進行檢驗。如果資料產生衝突了,則返回錯誤資訊,進行相應的處理。
實現:MySql最經常使用的樂觀鎖時進行版本控制,也就是在資料庫表中增加一列,記為version,當我們將資料讀出時,將版本號一併讀出,當資料進行更新時,會對這個版本號進行加1,當我們提交資料時,會判斷資料庫表中當前的version列值和當時讀出的version是否相同,若相同說明沒有進行更新的操作,不然,則取消這次的操作。
public class Test {
public static void main(String[] args) {
Test test = new Test();
Thread[] threads = new Thread[200];
for (int i=0;i<200;i++){
int finalI = i;
threads[i] = new Thread(){
@Override
public void run() {
test.service();
}
};
}
for (int i=0;i<200;i++){
threads[i].start();
}
}
public void service(){
try {
Connection connection = dbUtils.getConnection();
Stock stock1 = checkStock(connection);
updateCountByOpti(connection,stock1);
createOrder(connection);
}catch (Exception e){
System.out.println(e.getMessage());
}
}
private void createOrder(Connection connection) throws SQLException {
String insertSql = "insert into stock_order(sid,name) VALUES (1,'aaa')";
PreparedStatement statement = connection.prepareStatement(insertSql);
int insert = statement.executeUpdate();
}
private void updateCountByOpti(Connection connection,Stock stock) throws SQLException {
String sql = "update stock set count = count -1,version = version + 1 where version = " + stock.getVersion();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
int update = preparedStatement.executeUpdate();
if (update==0)
throw new RuntimeException("沒搶到");
}
public Stock checkStock(Connection connection) throws SQLException {
String sql = "select * from stock where id = 1";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
Stock stock = null;
if (resultSet.next()){
stock = new Stock();
stock.setId(resultSet.getInt("id"));
stock.setName(resultSet.getString("name"));
stock.setCount(resultSet.getInt("count"));
stock.setSale(resultSet.getInt("sale"));
stock.setVersion(resultSet.getInt("version"));
}
if (stock.getCount()<1)
throw new RuntimeException("沒有庫存了");
return stock;
}
}
上述在提交時,對version欄位進行了比較,當資料庫中的version和之前讀取的version一樣才會進行提交,否則提交失敗,接下來進行測試。
可以看到,只有10條記錄,樂觀鎖保證了資料的一致性。
三、悲觀鎖
MySql的悲觀鎖就是開啟事務,當啟動事務時,如果事務中的sql語句涉及到索引並用索引進行了條件判斷,那麼會使用行級鎖鎖定所要修改的行,否則使用表鎖鎖住整張表。
public class Test {
public static void main(String[] args) {
Test test = new Test();
Thread[] threads = new Thread[200];
for (int i=0;i<200;i++){
threads[i] = new Thread(){
@Override
public void run() {
try {
test.service();
} catch (SQLException e) {
e.printStackTrace();
}
}
};
}
for (int i=0;i<200;i++){
threads[i].start();
}
}
public void service() throws SQLException {
Connection connection = null;
try {
connection = dbUtils.getConnection();
connection.setAutoCommit(false);
Stock stock1 = checkStock(connection);
updateCountByOpti(connection,stock1);
createOrder(connection);
connection.commit();
}catch (Exception e){
System.out.println(e.getMessage());
connection.rollback();
}
}
private void createOrder(Connection connection) throws SQLException {
String insertSql = "insert into stock_order(sid,name) VALUES (1,'aaa')";
PreparedStatement statement = connection.prepareStatement(insertSql);
int insert = statement.executeUpdate();
}
private void updateCountByOpti(Connection connection,Stock stock) throws SQLException {
String sql = "update stock set count = count -1,version = version + 1 where version = " + stock.getVersion();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
int update = preparedStatement.executeUpdate();
if (update==0)
throw new RuntimeException("沒搶到");
}
public Stock checkStock(Connection connection) throws SQLException, InterruptedException {
String sql = "select * from stock where id = 1";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
Stock stock = null;
if (resultSet.next()){
stock = new Stock();
stock.setId(resultSet.getInt("id"));
stock.setName(resultSet.getString("name"));
stock.setCount(resultSet.getInt("count"));
stock.setSale(resultSet.getInt("sale"));
stock.setVersion(resultSet.getInt("version"));
}
if (stock.getCount()<1)
throw new RuntimeException("沒有庫存了");
return stock;
}
}
開啟事務並不難,所以使用悲觀鎖很簡單,讓我們看一下結果
結果還是10條記錄
我們可以在不同的場合使用不同的處理方法,樂觀鎖併發高並且效能也很好,而悲觀鎖雖然併發不是很高,但是它不允許髒讀,所以各有各的優點。