1. ホーム
  2. データベース
  3. マイサク

MySQLの悲観的ロックと楽観的ロックの実装スキーム

2022-01-18 03:28:52

前書き

悲観的ロックと楽観的ロックは、並行処理の問題を解決するために使われる2つの考え方で、それぞれ異なるプラットフォームで独自の実装がなされています。例えば、Javaではsynchronizedは悲観的ロックの実装(厳密ではなく、ヘビーウェイトロックにアップグレードする際にカウントされるロックアッププロセスがある)、Atomic***アトミッククラスは楽観的ロックの実装と考えることができる。

悲観的なロック

強い排他性と排他性を持ち、通常はシステムの相互排他によって、処理中もデータをロック状態に保つ。他のスレッドがロックを取得しようとすると、ロックを持っているスレッドがそれを解放するまでブロックされます。

楽観的ロック

データの変更とアクセスについて、競合が発生しないことを前提に楽観的に考える。データが更新のために送信されるときだけ、データの競合があるかどうかがテストされ、競合がなければ更新は成功し、そうでなければすぐに失敗し、ユーザーにエラーを返して次に何をするか選択させます。

MySQL 自身がロック機構をサポートしています。例えば、quot;check before write"の要件がある場合、処理全体を途中で中断できないアトミック操作にしたいのですが、これはクエリデータ行に "exclusive lock" を追加することで実現可能です。現在のトランザクションがロックを解放しない限り、MySQL は他のトランザクションが排他ロックを取得したい場合、現在のトランザクションがロックを解放するまでブロックします。この MySQL の排他的ロックは、「悲観的ロック」と呼ばれています。

MySQL 自体は楽観的ロック機能を提供していないため、開発者が自ら実装する必要があります。一般的な方法は、テーブルにバージョンカラムを追加し、データ行のバージョンをマークすることです。データを更新する必要がある場合、バージョンのバージョンを比較する必要があり、バージョンが一致していれば、この期間に他のトランザクションによってデータが変更されていないことを意味し、そうでなければ、他のトランザクションによってデータが変更されたことを意味し、スピンの再試行が必要となります。

ハンズオン

データベースには、商品テーブルと注文テーブルの2つのテーブルがあるとします。

ユーザーが注文した後、2つのアクションを実行する必要があります。

  1. 商品テーブルが在庫から減算される。
  2. Orders テーブルにレコードが作成されます。

初期データ:ID1の商品の在庫は100個で、注文テーブルのデータは空である。

クライアントは同時に発注するために10個のスレッドを起動し、それぞれロックフリー、悲観的ロック、楽観的ロックのシナリオでどのように動作するかを確認します。

以下は、テーブルを作成するためのSQL文です。

-- Goods table
CREATE TABLE `goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `goods_name` varchar(50) NOT NULL,
  `price` decimal(10,2) NOT NULL,
  `stock` int(11) DEFAULT '0',
  `version` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

-- order table
CREATE TABLE `t_order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `goods_id` bigint(20) NOT NULL,
  `order_time` datetime NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8


1. ロックなし

加工がない

// Place an order
private boolean order(){
    Goods goods = goodsMapper.selectById(1L);
    boolean success = false;
    if (goods.getStock() > 0) {
        goods.setStock(goods.getStock() - 1);
        // Update the inventory
        goodsMapper.updateById(goods);
        // Create an order
        orderMapper.save(goods.getId());
        success = true;
    }
    return success;
}


コンソール出力の結果

2.悲観的なロック

FOR UPDATEで項目を照会すると、データ行に排他的ロックがかかるので、他のスレッドが再度照会すると、現在のスレッドのトランザクションがコミットしてロックが解放され、他のスレッドが継続して発注できるようになるまでブロックされます。この方法は、高い同時実行性能を持ちません。

SQLステートメント

@Select("SELECT * FROM goods WHERE id = #{id} FOR UPDATE")
Goods selectForUpdate(Long id);


コンソール出力です。

注:FOR UPDATEはトランザクション内でなければ有効ではなく、クエリと更新は同じトランザクション内でなければなりません!!!

3. 最適化されたロック

更新のたびにバージョン番号をチェックし、バージョン番号が同じであれば、その期間中に他のスレッドによってデータが変更されていないので、現在のスレッドは普通に更新を送信することができます。

データを更新している間、バージョン番号は自己インクリメントでなければならない!!!!

@Update("UPDATE goods SET stock = #{stock},version = version+1 WHERE id = #{id} AND version = #{version}")
int updateByVersion(Long id, Integer stock, Integer version);


ビジネスコード

boolean order(){
    Goods goods = goodsMapper.selectById(1L);
    boolean success = false;
    if (goods.getStock() > 0) {
        goods.setStock(goods.getStock() - 1);
        // Update the stock with the version number
        int result = goodsMapper.updateByVersion(goods.getId(), goods.getStock(), goods.getVersion());
        if (result <= 0) {
            // The update failed, indicating that the data has been modified by other threads during the period and needs to be retried recursively
            return order();
        }
        // Create an order
        orderMapper.save(goods.getId());
        success = true;
    }
    return success;
}


コンソール出力の結果

概要

この記事は、MySQLの悲観的なロックと楽観的なロック方式を紹介し、より関連するMySQLの悲観的なロックと楽観的なロック内容は、スクリプト家の過去の記事を検索してくださいまたは次の関連記事を閲覧を継続し、あなたが将来的にスクリプト家をよりサポートすることを願っています