Spring Boot のタイムスタンプオプティミスティックロックモードを使用した高同時実行シーン処理の完全な例

著者は最近失業しました。以前は仕事が忙しくて、めったにブログを書くことがなかったのですが(主に怠け者だったので)、今ではどの会社でも自分を証明するためにブログ、オープンソース、Git などを持っていることが求められることを考えると、読者の皆様もたくさん応援していただければ幸いです!
JPA方式におけるバージョン方式を用いた楽観的ロックの仕組みについてはインターネット上で詳しく解説されており、Timestampタイムスタンプ方式について言及している資料もいくつかありますが、実際には最終的にはバージョン方式を採用しているものが多く、バージョンの楽観的ロック方式。これは良いことですが、データベース テーブルの設計では、多くの場合、別のバージョン フィールドを持たなければならず (作者は少し強迫的です)、これは美しくありません。実際のシナリオでは、タイムスタンプに似たフィールドが存在することがよくあります。 gmt_modified (Ali Java 仕様で一般的に使用されるデータベース フィールド仕様) などのテーブル列は、行データの最終更新時刻を記録するために使用されます。この記事では、この列を使用して同時実行性の高いシナリオでオプティミスティック ロック処理を実装する方法について説明します。(ところで、オプティミスティック ロックとは何ですか。Baidu にご自身で問い合わせてください)
この記事のすべての例は、Spring Boot フレームワークの下で実行されており、バージョンは 2.1.X です。

関連する依存ライブラリは次のとおりです。

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-actuator')
    implementation('org.springframework.boot:spring-boot-starter-cache')
    implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation('org.springframework.boot:spring-boot-starter-test')
    implementation('org.springframework.session:spring-session-core')
    compile('com.alibaba:druid:1.1.9')
    compile('com.alibaba:fastjson:1.2.47')
    compile('com.google.guava:guava:25.0-jre')
    compile('org.apache.commons:commons-text:1.6')
    compile('io.springfox:springfox-swagger2:2.8.0')
    compile('io.springfox:springfox-swagger-ui:2.8.0')
    compile('org.springframework.data:spring-data-elasticsearch:3.1.3.RELEASE')
    compile('com.aliyun.oss:aliyun-sdk-oss:3.3.0')
    compile('commons-fileupload:commons-fileupload:1.3.3')
    //compile('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE')
    compile('mysql:mysql-connector-java')
    compileOnly('org.springframework.boot:spring-boot-configuration-processor')
}

データシート

一般的に使用される製品テーブルを例にとると、テーブル構造は次のとおりです。
ここに画像の説明を挿入
フィールドは現在の実際のスタンプに従って更新されるように設定されていることに注意してください。デフォルト値は CURRENT_TIMESTAMP です。シナリオを検証するためにテスト データを挿入します。
ここに画像の説明を挿入
製品は書籍「JPA 高同時実行処理グラフィックスとテキストの詳細」、価格は 30 元、在庫は 3000 部です。

実在物

/**
 * @ClassName goods
 * @Description 商品表
 * @Author wangd
 * @Create 2018-12-25 11:28
 */
@Entity
@Table(name = "goods")
@Proxy(lazy = false)
public class Goods {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "name")
    private String name;

    @Column(name = "price")
    private int price;

    @Column(name = "stock")
    private int stock;

    @Version //版本控制注解
    @Column(name = "gmt_modified",columnDefinition="timestamp")//注解列名和列类型
    @Source(value = SourceType.DB) //注解值来自数据库
    private java.sql.Timestamp gmtModified;


    public long getId() {
        return id;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public Timestamp getGmtModified() {
        return gmtModified;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }
}

ダオ

サンプルはシンプルなので、コードを追加せずに直接設定できます。

/**
 * @InterFaceName GoodsDao
 * @Description 商品DAO
 * @Author wangd
 * @Create 2018-12-25 11:32
 */
public interface GoodsDao extends JpaRepository<Goods, Long> {

}

サービス

/**
 * @InterFaceName GoodsService
 * @Description 商品处理接口
 * @Author wangd
 * @Create 2018-12-25 11:34
 */
public interface IGoodsService {
    /**
     * 更改商品库存
     * @param id
     * @param decStock 减去库存数量
     * @return
     */
    Goods decreaseStock(long id,int decStock) throws InterruptedException;
}

サービスインターフェース実装クラス

/**
 * @ClassName GoodsServiceImpl
 * @Description 商品服务接口实现
 * @Author wangd
 * @Create 2018-12-25 11:36
 */
@Service
@Slf4j
public class IGoodsServiceImpl implements IGoodsService {
    @Autowired
    private GoodsDao goodsDao;

    @Override
    public Goods decreaseStock(long id, int decStock) throws InterruptedException{
        //首先获取商品信息
        Goods oldGood=goodsDao.getOne(id);
        try {
            //减库存
            if (oldGood.getStock() - decStock >= 0) {
                oldGood.setStock(oldGood.getStock() - decStock);
                return goodsDao.saveAndFlush(oldGood);
            }
        }catch (Exception e){
            throw new InterruptedException("删减库存"+decStock+"失败!并发冲突!");
        }
        //返回null值表示库存已被清空,实际场景可自行处理,这里只是示例
        return null;
    }
}

基本的な処理フレームワークが準備できました。オプティミスティック ロックが機能するかどうかを確認する必要があります。ここでは、JDK1.5 以降の強力な Java パッケージを並行して使用する必要があります。このパッケージは、非常にシンプルかつ強力な並行性サポートを提供します。具体的な導入については、このパッケージについては、ご自身でご理解ください。ここでは詳しく説明しません。
以下の JunitTest テスト クラスを構築します

/**
 * @ClassName IGoodsServiceTest
 * @Description 测试
 * @Author wangd
 * @Create 2018-12-25 12:09
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
@Slf4j
public class IGoodsServiceTest {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static int count = 0;

    @Autowired
    IGoodsService goodsService;

    @Test
    public void testDecreaseStock(){
        try {
            ExecutorService executorService = Executors.newCachedThreadPool();
            //信号量,此处用于控制并发的线程数
            final Semaphore semaphore = new Semaphore(threadTotal);
            //闭锁,可实现计数器递减
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal ; i++) {
                //使用lambada方式执行线程
                executorService.execute(() -> {
                    try {
                        //执行此方法用于获取执行许可,当总计未释放的许可数不超过200时,
                        //允许通行,否则线程阻塞等待,直到获取到许可。
                        semaphore.acquire();
                        //调用goodService的修改库存方法
                        Goods goods=goodsService.decreaseStock(1,25);
                        if(goods==null){
                            log.info("库存已经清空!");
                        }
                        //释放许可
                        semaphore.release();
                    } catch (InterruptedException e) {
                        //这里捕获IGoodsService抛出的中断异常,实际场景中可执行数据回滚操作
                        log.error("Exception", e);
                        e.printStackTrace();
                    }catch(Exception e){
                        log.error("exception", e);
                        e.printStackTrace();
                    }
                    //闭锁减一
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();//线程阻塞,直到闭锁值为0时,阻塞才释放,继续往下执行
            executorService.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

このテスト クラスを実行し、コンソール出力とデータベース レコードを観察すると、次の出力が表示されます。
ここに画像の説明を挿入
ブレークポイントを追跡し、ロックの競合によって引き起こされた例外かどうかを確認します。
ここに画像の説明を挿入
異常な ObjectOptimisticLockingFailureException がキャッチされ、この例外が発生したことを証明します。ロックの競合により。そして、goodsDao.saveAndFlush(oldGood);この文がロックの競合を引き起こし、私たちが示した例外をポップアップ表示します。インベントリが 0 に減った後も、多数の同時実行はまだ実行されていますが、テーブル データは更新されていないため、コンソールは次のように指示します。次の情報をスクロールして、ロックの競合をトリガーします。

ここに画像の説明を挿入
この時点でデータベース テーブルを確認します。

ここに画像の説明を挿入

シーン全体がシミュレーションされています。この例に不適切な点がある場合は、以下で議論して修正してください。

おすすめ

転載: blog.csdn.net/blackhost/article/details/85254928