É necessário um bloqueio ao gravar no banco de dados. Por exemplo, quando os dados são perdidos ao gravar no banco de dados ao mesmo tempo, um mecanismo de bloqueio é necessário.
Os bloqueios de dados são divididos em bloqueios otimistas e bloqueios pessimistas
Os cenários que eles usam são os seguintes:
-
O bloqueio otimista é adequado para o cenário de escrever menos e ler mais. Como esse bloqueio otimista é equivalente ao CAS de JAVA, quando vários dados vêm ao mesmo tempo, você pode retornar imediatamente sem esperar.
-
O bloqueio pessimista é adequado para o cenário de mais gravações e menos leituras. Esta situação também é equivalente a JAVA's synchronized, reentrantLock, etc. Quando uma grande quantidade de dados chega, apenas uma parte dos dados pode ser gravada e outros dados precisam esperar. Após a conclusão da execução, os próximos dados podem continuar.
Eles são diferentes na maneira como o fazem.
O bloqueio otimista usa o método do número da versão, ou seja, se o número da versão atual corresponder aos dados podem ser gravados, se o número da versão atual for considerado inconsistente, a atualização não terá sucesso, por exemplo
update table set column = value
where version=${version} and otherKey = ${otherKey}
O mecanismo de implementação de bloqueio pessimista geralmente é usado para atualização ao executar instruções de atualização, como
update table set column='value' for update
Nesse caso, a condição where deve envolver o campo de índice correspondente ao banco de dados, para que seja um bloqueio em nível de linha, caso contrário, será um bloqueio de tabela, o que tornará a velocidade de execução mais lenta.
Em seguida, obterei um projeto de boot de primavera (springboot 2.1.1 + mysql + [lombok] e, então, gradualmente realizarei o bloqueio otimista e o bloqueio pessimista.
Suponha que haja uma cena, haja uma tabela de catálogo de produtos do catálogo e, em seguida, haja uma tabela de navegação de navegação, se um produto é navegado, é necessário registrar quem navegou pelo usuário e registrar o número total de visitas.
A estrutura da tabela é muito simples:
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;
As dependências de POM.XML são as seguintes:
<?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>
A estrutura do projeto é a seguinte:
Apresente o conteúdo da estrutura do projeto:
-
Pacote de entidades: pacote de classes de entidades.
-
pacote de repositório: repositório de banco de dados
-
Pacote de serviços: prestação de serviços
-
Pacote do controlador: a gravação do controlador é usada para escrever o requestMapping. Aula de entrada do pedido relacionado
-
pacote de anotações: anotações personalizadas para tentar novamente.
-
pacote de aspectos: usado para aspectos de anotações personalizadas.
-
DblockApplication: A classe de inicialização do springboot.
-
DblockApplicationTests: classe de teste.
Vamos dar uma olhada na implementação do código principal. A referência é a seguinte. É muito conveniente usar dataJpa. CRUD simples pode ser realizado integrando CrudRepository. É muito conveniente e os alunos interessados podem estudar por conta própria.
Existem duas maneiras de implementar o bloqueio otimista:
-
Ao atualizar, passe o campo de versão, e ao atualizar, você pode julgar a versão.Se a versão corresponder, então você pode atualizar (método: updateCatalogWithVersion).
-
Ao adicionar versão ao campo de versão na classe de entidade, você pode corresponder e atualizar de acordo com a versão sem escrever instruções SQL. Não é muito simples?
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);
}
Existem também duas maneiras de obter bloqueio pessimista:
-
Escreva o SQL nativo sozinho e, em seguida, escreva para a instrução de atualização. (Método: findCatalogsForUpdate)
-
Use a anotação @Lock e defina o valor para LockModeType.PESSIMISTIC_WRITE para representar o bloqueio em nível de linha.
Também existe uma classe de teste que escrevi para facilitar seus testes:
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();
}
}
}
Chamado 100 vezes, ou seja, um produto pode ser navegado cem vezes, usando o bloqueio pessimista, os dados da tabela de catálogo são 100 e a tabela de navegação também é de 100 registros. Quando o bloqueio otimista é usado, alguns registros serão perdidos devido ao relacionamento de correspondência dos números de versão, mas os dados nessas duas tabelas podem ser correspondentes.
Após a falha do bloqueio otimista, ObjectOptimisticLockingFailureException será lançado, então vamos considerar uma nova tentativa para esta parte. Abaixo, personalizarei uma anotação para o aspecto.
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 {
}
Para as anotações, consulte o código a seguir. Defino o número máximo de tentativas como 5 e paro de tentar novamente depois de mais de 5 vezes.
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;
}
}
A ideia geral é esta.