データベースへの書き込み時にはロックが必要です。たとえば、データベースへの書き込みと同時にデータが失われる場合は、ロック機構が必要です。
データロックは、楽観的ロックと悲観的ロックに分けられます
彼らが使用するシナリオは次のとおりです。
-
オプティミスティックロックは、書き込みを減らして読み取りを増やすシナリオに適しています。このオプティミスティックロックはJAVAのCASと同等であるため、複数のデータが同時に来ると、待たずにすぐに戻ることができます。
-
ペシミスティックロックは、書き込みが多く読み取りが少ないシナリオに適しています。この状況は、JAVAの同期、reentrantLockなどにも相当します。大量のデータが来ると、1つのデータしか書き込むことができず、他のデータは待機する必要があります。実行が完了した後、次のデータを続行できます。
彼らはそれをする方法が異なります。
オプティミスティックロックはバージョン番号方式を使用します。つまり、現在のバージョン番号がデータに対応している場合、現在のバージョン番号に一貫性がないと判断された場合、更新は成功しません。
update table set column = value
where version=${version} and otherKey = ${otherKey}
悲観的なロックの実装のメカニズムは、通常、次のような更新ステートメントを実行するときに更新に使用することです。
update table set column='value' for update
この場合、where条件には、データベースに対応するインデックスフィールドが含まれている必要があります。これにより、行レベルのロックになります。そうでない場合は、テーブルロックになり、実行速度が低下します。
次に、スプリングブート(springboot 2.1.1 + mysql + [lombok]プロジェクト)を取得し、楽観的ロックと悲観的ロックを徐々に実現します。
シーンがあり、カタログ製品カタログテーブルがあり、次に参照参照テーブルがあるとします。製品を参照する場合は、ユーザーを参照しているユーザーを記録し、合計訪問数を記録する必要があります。
テーブルの構造は非常に単純です。
create table catalog (
id int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
name varchar(50) NOT NULL DEFAULT '' COMMENT '商品名称',
browse_count int(11) NOT NULL DEFAULT 0 COMMENT '浏览数',
version int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁,版本号',
PRIMARY KEY(id)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE table browse (
id int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
cata_id int(11) NOT NULL COMMENT '商品ID',
user varchar(50) NOT NULL DEFAULT '' COMMENT '',
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY(id)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
POM.XMLの依存関係は次のとおりです。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hqs</groupId>
<artifactId>dblock</artifactId>
<version>1.0-SNAPSHOT</version>
<name>dblock</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
プロジェクトの構成は次のとおりです。
プロジェクト構造の内容を紹介します。
-
エンティティパッケージ:エンティティクラスパッケージ。
-
リポジトリパッケージ:データベースリポジトリ
-
サービスパッケージ:サービス提供サービス
-
コントローラパッケージ:コントローラ書き込みは、requestMappingの書き込みに使用されます。関連リクエストのエントランスクラス
-
注釈パッケージ:再試行用のカスタム注釈。
-
アスペクトパッケージ:カスタム注釈をアスペクトするために使用されます。
-
DblockApplication:springbootのスタートアップクラス。
-
DblockApplicationTests:テストクラス。
コアコードの実装を見てみましょう。リファレンスは次のとおりです。dataJpaを使用すると非常に便利です。CrudRepositoryを統合することで簡単なCRUDを実現できます。非常に便利で、興味のある学生は自分で勉強できます。
楽観的ロックを実装するには、次の2つの方法があります。
-
更新時にバージョンフィールドを渡すと、更新時にバージョン判定を実行できます。バージョンが一致する場合は、更新できます(メソッド:updateCatalogWithVersion)。
-
エンティティクラスのバージョンフィールドにバージョンを追加することで、SQLステートメントを自分で記述しなくても、バージョンに応じて照合および更新できます。非常に簡単ではありませんか。
public interface CatalogRepository extends CrudRepository<Catalog, Long> {
@Query(value = "select * from Catalog a where a.id = :id for update", nativeQuery = true)
Optional<Catalog> findCatalogsForUpdate(@Param("id") Long id);
@Lock(value = LockModeType.PESSIMISTIC_WRITE) //代表行级锁
@Query("select a from Catalog a where a.id = :id")
Optional<Catalog> findCatalogWithPessimisticLock(@Param("id") Long id);
@Modifying(clearAutomatically = true) //修改时需要带上
@Query(value = "update Catalog set browse_count = :browseCount, version = version + 1 where id = :id " +
"and version = :version", nativeQuery = true)
int updateCatalogWithVersion(@Param("id") Long id, @Param("browseCount") Long browseCount, @Param("version") Long version);
}
悲観的なロックを実現するには、次の2つの方法もあります。
-
ネイティブSQLを自分で作成してから、updateステートメント用に作成します。(メソッド:findCatalogsForUpdate)
-
@Lockアノテーションを使用し、値をLockModeType.PESSIMISTIC_WRITEに設定して、行レベルのロックを表します。
テストを容易にするために私が書いたテストクラスもあります。
package com.hqs.dblock;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = DblockApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DblockApplicationTests {
@Autowired
private TestRestTemplate testRestTemplate;
@Test
public void browseCatalogTest() {
String url = "http://localhost:8888/catalog";
for(int i = 0; i < 100; i++) {
final int num = i;
new Thread(() -> {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("catalogId", "1");
params.add("user", "user" + num);
String result = testRestTemplate.postForObject(url, params, String.class);
System.out.println("-------------" + result);
}
).start();
}
}
@Test
public void browseCatalogTestRetry() {
String url = "http://localhost:8888/catalogRetry";
for(int i = 0; i < 100; i++) {
final int num = i;
new Thread(() -> {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("catalogId", "1");
params.add("user", "user" + num);
String result = testRestTemplate.postForObject(url, params, String.class);
System.out.println("-------------" + result);
}
).start();
}
}
}
100回呼び出されます。つまり、ペシミスティックロックを使用して製品を100回参照でき、カタログテーブルのデータは100であり、参照テーブルも100レコードです。楽観的ロックを使用すると、バージョン番号の一致関係のために一部のレコードが失われますが、これら2つのテーブルのデータは対応している可能性があります。
楽観的ロックが失敗した後、ObjectOptimisticLockingFailureExceptionがスローされるので、この部分の再試行を検討しましょう。以下では、アスペクトの注釈をカスタマイズします。
package com.hqs.dblock.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryOnFailure {
}
注釈については、次のコードを参照してください。再試行の最大回数を5に設定し、5回以上再試行を停止しました。
package com.hqs.dblock.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.hibernate.StaleObjectStateException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class RetryAspect {
public static final int MAX_RETRY_TIMES = 5;//max retry times
@Pointcut("@annotation(com.hqs.dblock.annotation.RetryOnFailure)") //self-defined pointcount for RetryOnFailure
public void retryOnFailure(){}
@Around("retryOnFailure()") //around can be execute before and after the point
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int attempts = 0;
do {
attempts++;
try {
pjp.proceed();
} catch (Exception e) {
if(e instanceof ObjectOptimisticLockingFailureException ||
e instanceof StaleObjectStateException) {
log.info("retrying....times:{}", attempts);
if(attempts > MAX_RETRY_TIMES) {
log.info("retry excceed the max times..");
throw e;
}
}
}
} while (attempts < MAX_RETRY_TIMES);
return null;
}
}
一般的な考え方はこれです。