在写入数据库的时候需要有锁, 比如同时写入数据库的时候会出现丢数据, 那么就需要锁机制.
数据锁分为乐观锁和悲观锁
它们使用的场景如下:
乐观锁适用于写少读多的情景, 因为这种乐观锁相当于 JAVA 的 CAS, 所以多条数据同时过来的时候, 不用等待, 可以立即进行返回.
悲观锁适用于写多读少的情景, 这种情况也相当于 JAVA 的 synchronized,reentrantLock 等, 大量数据过来的时候, 只有一条数据可以被写入, 其他的数据需要等待. 执行完成后下一条数据可以继续.
他们实现的方式上有所不同.
乐观锁采用版本号的方式, 即当前版本号如果对应上了就可以写入数据, 如果判断当前版本号不一致, 那么就不会更新成功,
比如
- update table set column = value
- where version=${
- version
- } and otherKey = ${
- otherKey
- }
悲观锁实现的机制一般是在执行更新语句的时候采用 for update 方式,
比如
update table set column='value' for update
这种情况 where 条件呢一定要涉及到数据库对应的索引字段, 这样才会是行级锁, 否则会是表锁, 这样执行速度会变慢.
下面我就弄一个 spring boot(springboot 2.1.1 + MySQL + lombok + aop + jpa) 工程, 然后逐渐的实现乐观锁和悲观锁.
假设有一个场景, 有一个 catalog 商品目录表, 然后还有一个 browse 浏览表, 假如一个商品被浏览了, 那么就需要记录下浏览的 user 是谁, 并且记录访问的总数.
表的结构非常简单:
- 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>
项目的结构如下:
介绍一下项目的结构的内容:
entity 包: 实体类包.
repository 包: 数据库 repository
service 包: 提供服务的 service
controller 包: 控制器写入用于编写 requestMapping. 相关请求的入口类
annotation 包: 自定义注解, 用于重试.
aspect 包: 用于对自定义注解进行切面.
DblockApplication:springboot 的启动类.
DblockApplicationTests: 测试类.
咱们看一下核心代码的实现, 参考如下, 使用 dataJpa 非常方便, 集成了 CrudRepository 就可以实现简单的 CRUD, 非常方便, 有兴趣的同学可以自行研究.
实现乐观锁的方式有两种:
更新的时候将 version 字段传过来, 然后更新的时候就可以进行 version 判断, 如果 version 可以匹配上, 那么就可以更新 (方法: updateCatalogWithVersion).
在实体类上的 version 字段上加入 version, 可以不用自己写 SQL 语句就可以它就可以自行的按照 version 匹配和更新, 是不是很简单.
- 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);
- }
实现悲观锁的时候也有两种方式:
自行写原生 SQL, 然后写上 for 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 次, 即一个商品可以浏览一百次, 采用悲观锁, catalog 表的数据都是 100, 并且 browse 表也是 100 条记录. 采用乐观锁的时候, 因为版本号的匹配关系, 那么会有一些记录丢失, 但是这两个表的数据是可以对应上的.
乐观锁失败后会抛出 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;
- }
- }
来源: http://www.jianshu.com/p/7824cec41e98