Spring Cloud Alibaba微服务项目中Redis实现分布式事务锁实践

2022-01-06 20:53:39 浏览数 (1)

引言

我们知道同一个进程里面为了解决资源共享而不出现高并发的问题可以通过高并发编程解决,通过给变量添加volatile关键字实现线程间变量可见;通过synchronized关键字修饰代码块、对象或者方法以及通过调用java.util.current包下的API显式地加锁和释放锁操作都实现多线程场景下的同步处理。

但是当服务器部署了多台以后,对于控制不同JVM进程下的多线程高并发访问就会失效。无论是通过给变量添加volatile关键字,还是在控制并发访问的代码块中对一个对象锁加synchronized关键字,抑或是通过调用java.util.current包下的API显式地加锁和释放锁都无法解决分布式场景下不同JVM进程中的多线程并发访问同步的问题。典型的如电商场景中的秒杀、下单和减库存操作,订单服务和库存服务都属于不同的微服务,每个微服务都会有多个实例。

这个时候就需要引入分布式事务锁方案来解决问题了,分布式事务锁主要有rediszookeeper数据库版本锁(也叫乐观锁)三种常用的实现方式。其中以redis实现分布式事务锁用起来最简单高效, redis实现分布式事务锁主要是通过它的setnx命令以及执行lua脚本实现原子操作来实现分布式事务锁,另外redis客户端也以及提供了redission更高级的实现分布式事务锁的用法。只不过redission实现分布式事务锁的底层也是基于执行lua脚本实现的。

为了控制文章篇幅,也为了让本位具有值得各位读者仔细一看的干货内容,本文内容只涉及在springboot微服务项目中通过redis客户端执行setnx命令和执行lua脚本来实现。另两种方式笔者有时间了再来另外通过实战的方式撰文讲解。

1 Redis实现分布式事务锁的原理

redis之所以能实现分布式事务锁是因为它是一个全局数据库,而且它是一个key-value形式的NO-SQL数据库,对于不同jvm进程中的多线程执行同一段代码时可以实现全局加锁和释放锁操作。setnx命令是判断redis缓存中是否有这个key, 没有才set成功,set成功表示拿到了分布式锁,可以进行后面需要控制并发访问的逻辑。为了防止加锁的机器宕机造成的死锁问题可以通过redis对缓存key 设置过期时间来解决;而执行lua脚本是一个原子操作,同一时间只能有一个客户端在执行,这对于保证分布式高并发场景下事务的原子性和一致性是非常必要的。因此通过执行lua脚本实现分布式事务锁就成为了一个非常好的解决方案。

2 搭建集成Redis的微服务项目

Spring Redis要求Redis 2.6 以上版本,Spring Data 通过 JedisLettuce 两个Java开源类库与Redis集成, 无论使用哪种客户端,你要用到spring-data-redisjar包中org.springframework.data.redis.connection包下的两个抽象接口RedisConnectionRedisConnectionFactory用于获得于Redis交互的工作连接。JedisLettuce两个类库提供了RedisConnectionFactory接口的实现类LettuceConnectionFactoryJedisConnectionFactory

spring-boot-starter-data-redis起步依赖里面默认使用的客户端是Lettuce客户端,只是很多人习惯使用Jedis客户端操作Redis, 因为使用jedis客户端操作redis命令更接近原生的redis命令用法。

2.1 redis自动配置介绍

spring-boot项目中的redis自动配置类位于org.springframework.boot.autoconfigure.data.redis包下的RedisAutoConfiguration类,这个自动配置类的源码如下:

代码语言:javascript复制
@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

它会根据RedisProperties属性配置类中的配置信息实例化redis连接对象,并自动导入LettuceConnectionConfigurationJedisConnectionConfiguration两个配置类。同时在项目中缺失两个bean的情况情况下,向Spring IOC容器中实例化并注入RedisTemplateStringRedisTemplate两个bean。

2.1 微服务项目骨架搭建

在我的上一篇有关微服务实践的文章记一次使用Nacos 2.0.3版本搭建微服务注册中心和客户端的踩坑填坑详细过程项目的基础上搭建微服务聚合项目alibaba-demos。增加三个子模块项目:alibaba-commons(公共模块项目), alibaba-service-provider(微服务提供者模块项目),alibaba-service-consumer(微服务消费者模块项目)。

alibaba-service-provider项目模拟电商库存服务,alibaba-service-consumer项目模拟电商订单服务,两个微服务均对外提供web服务。

2.2 项目maven依赖

  1. alibaba-demos项目pom.xml
代码语言:javascript复制
<?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>

    <groupId>com.spring.cloud</groupId>
    <artifactId>alibaba-demos</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>alibaba-commons</module>
        <module>alibaba-service-provider</module>
        <module>alibaba-service-consumer</module>
    </modules>
    <name>alibaba-demos</name>
    <description>spring cloud alibaba demos</description>
    <packaging>pom</packaging>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.2.7.RELEASE</spring-boot.version>
        <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-test</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-autoconfigure</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>2.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.spring</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>1.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
            <version>2.2.0.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-consul-discovery</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.aspectj</groupId>
                    <artifactId>aspectjweaver</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
        </dependency>
        <dependency>
            <groupId>org.reflections</groupId>
            <artifactId>reflections</artifactId>
            <version>0.9.10</version>
        </dependency>
        <dependency>
            <groupId>io.prometheus</groupId>
            <artifactId>simpleclient</artifactId>
            <version>0.0.9</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.20</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. alibaba-commons模块项目pom.xml
代码语言:javascript复制
<?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">
    <parent>
        <artifactId>alibaba-demos</artifactId>
        <groupId>com.spring.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>alibaba-commons</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.5</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.5</version>
        </dependency>
    </dependencies>
</project>

  1. alibaba-service-provider模块项目pom.xml
代码语言:javascript复制
<?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">
    <parent>
        <artifactId>alibaba-demos</artifactId>
        <groupId>com.spring.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>alibaba-service-provider</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.spring.cloud</groupId>
            <artifactId>alibaba-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>org.aspectj</groupId>
                    <artifactId>aspectjweaver</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba.spring</groupId>
                    <artifactId>spring-context-support</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba.nacos</groupId>
                    <artifactId>nacos-client</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.reflections</groupId>
                    <artifactId>reflections</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.prometheus</groupId>
                    <artifactId>simpleclient</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.2.RELEASE</version>
                <configuration>
                    <mainClass></mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

    </build>
</project>
  1. aliba-service-consumer模块项目pom.xml
代码语言:javascript复制
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.spring.cloud</groupId>
    <artifactId>alibaba-consumer</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>alibaba-consumer</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <artifactId>alibaba-demos</artifactId>
        <groupId>com.spring.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-autoconfigure</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-keyvalue</artifactId>
            <version>2.5.6</version>
        </dependency>
        <dependency>
            <groupId>com.spring.cloud</groupId>
            <artifactId>alibaba-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>org.aspectj</groupId>
                    <artifactId>aspectjweaver</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba.spring</groupId>
                    <artifactId>spring-context-support</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba.nacos</groupId>
                    <artifactId>nacos-client</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.reflections</groupId>
                    <artifactId>reflections</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.prometheus</groupId>
                    <artifactId>simpleclient</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.xmlunit</groupId>
                    <artifactId>xmlunit-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.xmlunit</groupId>
            <artifactId>xmlunit-core</artifactId>
            <version>2.6.2</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.2.7.RELEASE</version>
                <configuration>
                    <mainClass>com.spring.cloud.alibabaconsumer.AlibabaConsumerApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

2.3 项目配置类

1 alibaba-service-provider项目application.properties

代码语言:javascript复制
server.port=9000
server.servlet.context-path=/services
spring.profiles.active=dev
spring.jackson.time-zone=GMT 8
spring.devtools.add-properties=false

mybatis-plus.mapper-locations=classpath:com/spring/cloud/alibaba/service/provider/mapper/*Mapper.xml
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

2 alibaba-service-provider项目application-dev.properties

代码语言:javascript复制
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=heshengfu2018

logging.level.root.com.apache.ibatis=trace
logging.level.root.java.sql.Connection=debug
logging.level.java.sql.Statement=info
logging.level.java.sql.PreparedStatement=info

3 alibaba-service-provider项目bootstrap.properties,将库存服务注册到注册中心

代码语言:javascript复制
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.namespace=public
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=stock-service

4 alibaba-service-consumer项目application.properties文件

代码语言:javascript复制
# 应用服务 WEB 访问端口
server.port=9002
server.servlet.context-path=/order-service
spring.devtools.add-properties=false
spring.profiles.active=dev
spring.jackson.time-zone=GMT 8
mybatis-plus.mapper-locations=classpath:com/spring/cloud/alibabaconsumer/mapper/*Mapper.xml
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# redis配置
spring.redis.client-name=redis-client
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=5000ms
spring.redis.jedis.pool.min-idle=1
spring.redis.jedis.pool.time-between-eviction-runs=30000ms

#微服务url
stock.service.query-stock-url=http://stock-service/services/stock/findStockByCode
stock.service.update-count-url=http://stock-service/services/stock/updateStockCountById

5 alibaba-service-consumer项目application-dev.properties

代码语言:javascript复制
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/vueblog2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=vueblog
spring.datasource.password=vueblog2021#

logging.level.root.com.apache.ibatis=trace
logging.level.root.java.sql.Connection=debug
logging.level.java.sql.Statement=info
logging.level.java.sql.PreparedStatement=info

库存服务与订单服务的关系及创建订单的流程,笔者画了一幅如下所示的简单流程图

2.4 数据库建表与创建实体类

  1. 新建库存表并添加数据

打开navicat客户端新建连接,使用root账户和登录密码连接本地MySQL服务test数据库后在控制台中执行以下sql脚本

代码语言:javascript复制
DROP TABLE IF EXISTS `stock_info`;
CREATE TABLE `stock_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `good_code` varchar(30) NOT NULL COMMENT '商品编码',
  `good_name` varchar(100) DEFAULT NULL COMMENT '商品名称',
  `count` int(11) DEFAULT '0' COMMENT '商品数量',
  `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `created_by` varchar(30) NOT NULL DEFAULT 'system' COMMENT '创建人',
  `last_updated_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
  `last_updated_by` varchar(30) NOT NULL DEFAULT 'system' COMMENT '最后更新人',
  `unit_price` int(11) DEFAULT '0' COMMENT '单价,单位分',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_good_code` (`good_code`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of stock_info
-- ----------------------------
INSERT INTO `stock_info` VALUES ('1', 'huawei_mate3', '华为手机mate3', '1000', '2021-11-08 23:42:02', 'heshengfu', '2021-11-21 21:11:08', 'heshengfu', '200000');
INSERT INTO `stock_info` VALUES ('2', 'huawei_mate5', '华为手机mate5', '1000', '2021-11-08 23:42:02', 'heshengfu', '2021-11-21 21:11:08', 'heshengfu', '300000');
INSERT INTO `stock_info` VALUES ('3', 'iphone_plus8', '苹果手机plus8', '1000', '2021-11-08 23:42:02', 'heshengfu', '2021-11-21 21:11:08', 'heshengfu', '500000');
INSERT INTO `stock_info` VALUES ('4', 'iphone_11', '苹果手机11', '860', '2021-11-08 23:42:02', 'heshengfu', '2022-01-03 14:26:58', 'system', '650000');
INSERT INTO `stock_info` VALUES ('5', 'iphone_12', '苹果手机12', '1000', '2021-11-08 23:42:02', 'heshengfu', '2021-11-21 21:11:08', 'heshengfu', '700000');
INSERT INTO `stock_info` VALUES ('6', 'iphone_13', '苹果手机13', '1000', '2021-11-08 23:42:02', 'heshengfu', '2021-11-21 21:11:08', 'heshengfu', '800000');
INSERT INTO `stock_info` VALUES ('7', 'xiaomi_note3', '小米手机note3', '500', '2021-11-28 20:21:23', 'system', '2021-11-28 20:21:23', 'system', '200000');
INSERT INTO `stock_info` VALUES ('8', 'xiaomi_note4', '小米手机note4', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '280000');
INSERT INTO `stock_info` VALUES ('9', 'xioami_note5', '小米手机note5', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '300000');
INSERT INTO `stock_info` VALUES ('10', 'xiaomi_note6', '小米手机note6', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '330000');
INSERT INTO `stock_info` VALUES ('11', 'xiaomi_note7', '小米手机note7', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '350000');
INSERT INTO `stock_info` VALUES ('12', 'xiaomi_note8', '小米手机note8', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '380000');
INSERT INTO `stock_info` VALUES ('13', 'honor50', '荣耀50', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '219900');
INSERT INTO `stock_info` VALUES ('14', 'honor50_SE', '荣耀50SE', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '219900');
INSERT INTO `stock_info` VALUES ('15', 'honor50Pro', '荣耀50Pro', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '349900');
INSERT INTO `stock_info` VALUES ('16', 'honorX10', '荣耀X10', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '179900');
INSERT INTO `stock_info` VALUES ('17', 'honorX30_Max', '荣耀X30_Max', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '239900');
INSERT INTO `stock_info` VALUES ('18', 'honorX30_Magic3_drag888', '荣耀X30_Magic3_骁龙888', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '469900');
INSERT INTO `stock_info` VALUES ('19', 'honorX30_Magic3_Pro', '荣耀X30_Magic3_Pro', '500', '2021-11-28 20:42:19', 'system', '2021-11-28 20:42:19', 'system', '469900');
INSERT INTO `stock_info` VALUES ('20', 'meizu', '魅族手机', '500', '2021-11-30 02:05:15', 'system', '2021-11-30 02:05:15', 'system', '200000');
INSERT INTO `stock_info` VALUES ('21', 'meizu3', '魅族手机', '500', '2021-11-30 02:07:46', 'system', '2021-11-30 02:07:46', 'system', '200000');
INSERT INTO `stock_info` VALUES ('22', 'GalaxyNote20', '三星Noto20', '500', '2021-12-04 16:22:32', 'system', '2021-12-04 16:22:32', 'system', '589900');
INSERT INTO `stock_info` VALUES ('23', 'GalaxyNote3', '三星Note3', '500', '2021-12-04 16:36:50', 'system', '2021-12-04 16:36:50', 'system', '280000');
INSERT INTO `stock_info` VALUES ('24', 'GalaxyNote4', '三星Note4', '500', '2021-12-04 16:36:50', 'system', '2021-12-04 16:36:50', 'system', '300000');
INSERT INTO `stock_info` VALUES ('25', 'GalaxyNote5', '三星Note4', '500', '2021-12-04 16:36:50', 'system', '2021-12-04 16:36:50', 'system', '330000');
INSERT INTO `stock_info` VALUES ('26', 'GalaxyNote6', '三星Note6', '500', '2021-12-04 16:36:50', 'system', '2021-12-04 16:36:50', 'system', '350000');
INSERT INTO `stock_info` VALUES ('27', 'GalaxyNote7', '三星Note7', '500', '2021-12-04 16:36:50', 'system', '2021-12-04 16:36:50', 'system', '380000');
  1. 同样打开navicat客户端新建连接,使用vueblog用户和登录密码连接MySQL服务vueblog2数据库后执行订单表的创建脚本
代码语言:javascript复制
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
  `order_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_id` bigint(20) NOT NULL,
  `order_no` varchar(50) NOT NULL COMMENT '订单编号',
  `good_code` varchar(30) NOT NULL COMMENT '商品码',
  `good_count` int(11) NOT NULL DEFAULT '1' COMMENT '订单数量',
  `order_money` bigint(20) NOT NULL DEFAULT '0' COMMENT '订单金额,单位分',
  `created_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `created_by` varchar(30) NOT NULL DEFAULT 'system' COMMENT '创建人',
  `last_updated_date` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '上次修改时间',
  `last_updated_by` varchar(30) DEFAULT 'system' COMMENT '上次修改人',
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;
  1. alibaba-commons模块下的com.spring.cloud.alibaba.commons.pojo包下新建与以上两个数据库对于的实体类

StockInfo.java

代码语言:javascript复制
@Data
@TableName("stock_info")
public class StockInfo extends BaseEntity {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 商品代码
     */
    @TableField(value = "good_code")
    private String goodCode;

    /**
     * 商品名称
     */
    @TableField(value = "good_name")
    private String goodName;

    /**
     * 库存数量
     */
    @TableField(value = "count")
    private Integer count;

    /**
     * 商品单价,单位:分
     */
    @TableField(value = "unit_price")
    private Long unitPrice;


}

OrderInfo.java

代码语言:javascript复制
@Data
@TableName("orders")
public class OrderInfo extends BaseEntity {
    @TableId(type=IdType.AUTO)
    private Long orderId;

    @TableField(value="user_id")
    private Long userId;

    @TableField(value = "order_no")
    private String orderNo;

    @TableField(value = "good_code")
    private String goodCode;

    @TableField(value = "good_count")
    private int goodCount;

    @TableField(value = "order_money")
    private Long orderMoney;
}

BaseEntity.java

代码语言:javascript复制
@Data
public class BaseEntity implements Serializable {

    /**
     * 创建人
     */
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;

    /**
     * 创建日期(带时间)
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(value = "created_date", fill = FieldFill.INSERT)
    private Date createdDate;

    /**
     * 修改人用户ID
     */
    @TableField(value = "last_updated_by", fill = FieldFill.INSERT_UPDATE)
    private String lastUpdatedBy;

    /**
     * 修改日期(带时间)
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(value = "last_updated_date", fill = FieldFill.INSERT_UPDATE)
    private Date lastUpdatedDate;
}

2.5 库存微服务编码

  1. 启动类ServiceProviderApplication.java
代码语言:javascript复制
@SpringBootApplication(scanBasePackages = {"com.spring.cloud.alibaba.commons",
        "com.spring.cloud.alibaba.service.provider"})
@MapperScan(basePackages = "com.spring.cloud.alibaba.service.provider.mapper")
@EnableDiscoveryClient
public class ServiceProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceProviderApplication.class, args);
    }
}

@EnableDiscoveryClient注解用于开启微服务自动发现功能

  1. MybatisPlus分页配置类
代码语言:javascript复制
@Configuration
public class MybatisPlusConfig {

     @Bean
     public PaginationInterceptor paginationInterceptor() {
         PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
         paginationInterceptor.setOverflow(true);
         paginationInterceptor.setDialectClazz("com.baomidou.mybatisplus.extension.plugins.pagination.dialects.MySqlDialect");
         paginationInterceptor.setSqlParser(new JsqlParserCountOptimize());
         return paginationInterceptor;
     }
}

3) Mapper层编码

这里我们选用MybatisPlus作为持久层框架,通过继承BaseMapper可直接获得基本的数据库CRUD方法。

代码语言:javascript复制
@Repository
public interface StockMapper extends BaseMapper<StockInfo> {

}
  1. Service层编码
代码语言:javascript复制
public interface IStockService extends IService<StockInfo> {
    /**
    * 通过商品编码查找库存
    */
    ResponseVo findStockByGoodCode(String goodCode);
    /**
    * 修改库存
    */
    ResponseVo  updateStockById(StockInfo stockInfo);
}
  1. 控制器层编码
代码语言:javascript复制
@RestController
@RequestMapping("/stock")
@RefreshScope
public class StockController {

    @Resource
    private IStockService stockService;

    /**
    * 通过商品编码查找库存
    */
    @GetMapping(value = "/findStockByCode")
    public ResponseVo findStockByGoodCode(@RequestParam("goodCode") String goodCode){
        if(StringUtils.isEmpty(goodCode)) {
            throw new IllegalArgumentException("parameter goodCode cannot be null");
        }
        return stockService.findStockByGoodCode(goodCode);
    }
    /**
    * 修改库存
    */
    @PostMapping("/updateStockCountById")
    public ResponseVo updateStockById(@RequestBody StockInfo stockInfo){
        if(stockInfo.getId()==null || stockInfo.getId()<=0){
            throw new IllegalArgumentException("parameter id cannot small than 0");
        }
        if(stockInfo.getCount() < 0) {
            throw new IllegalArgumentException("parameter count cannot small than 0");
        }
        return stockService.updateStockById(stockInfo);
    }
}

2.6 订单服务编码

1) 启动类AlibabaConsumerApplication.java

代码语言:javascript复制
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.spring.cloud.alibabaconsumer.mapper")
public class AlibabaConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AlibabaConsumerApplication.class, args);
    }
}
  1. 配置类

RestTemplateConfig类用于构造实现httphttps协议的远程服务调用的RestTemplate模板工具类bean。

代码语言:javascript复制
@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){
        return restTemplateBuilder.build();
    }
}

TaskPoolConfig类用于构造自定义线程池,用户下单成功后异步见库存

代码语言:javascript复制
package com.spring.cloud.alibabaconsumer.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Configuration
public class TaskPoolConfig {

    /**
     * 自定义线程池
     * @return ThreadPoolExecutor
     */
    @Bean(name = "customTaskWorkPoolExecutor")
    public ThreadPoolExecutor customTaskWorkPoolExecutor() {
        ArrayBlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue(25);
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 50, 30000, TimeUnit.MILLISECONDS, taskQueue);
        return threadPoolExecutor;
    }
}
  1. 持久层编码
代码语言:javascript复制
package com.spring.cloud.alibabaconsumer.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.spring.cloud.alibaba.commons.pojo.OrderInfo;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderMapper extends BaseMapper<OrderInfo> {

}
  1. 服务层编码

服务层主要实现创建订单方法

代码语言:javascript复制
public interface OrderService extends IService<OrderInfo> {

    ResponseVo createOrder(OrderInfo orderEntity, Integer flag);
}
代码语言:javascript复制
package com.spring.cloud.alibabaconsumer.service.impl;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.spring.cloud.alibaba.commons.pojo.OrderInfo;
import com.spring.cloud.alibaba.commons.pojo.ResponseVo;
import com.spring.cloud.alibaba.commons.pojo.StockInfo;
import com.spring.cloud.alibabaconsumer.mapper.OrderMapper;
import com.spring.cloud.alibabaconsumer.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import redis.clients.jedis.Jedis;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, OrderInfo> implements OrderService {

    private final static Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);

    private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss.SSS");

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Resource(name="customTaskWorkPoolExecutor")
    private ThreadPoolExecutor threadPoolExecutor;

    @Value("${stock.service.query-stock-url}")
    private String queryGoodStockServiceUrl;

    @Value("${stock.service.update-count-url}")
    private String updateStockCountUrl;
    /**
    * 通过flag参数控制执行释放锁的方式
    * @param orderEntity 订单实体类
    * @param flag 释放锁方式标识:1-RedisTemplate#del(key)方式释放锁;2-Jedis#eval方法执行lua脚本释放锁;3-RedisTemplate#execute方法执行lua脚本释放锁
    */
    @Override
    public ResponseVo createOrder(OrderInfo orderEntity, Integer flag) {
        ResponseVo responseVo;
        if (flag == 1 || flag == 2) {
            responseVo = setNxLock(orderEntity, flag);
        } else {
            responseVo = redisTemplateLock(orderEntity);
        }
        return responseVo;
    }

    private void completeOrderInfo(OrderInfo orderInfo) {
        if (orderInfo.getUserId() == null) {
            orderInfo.setUserId(1L);
        }
        String orderNo = sdf.format(new Date(System.currentTimeMillis()));
        logger.info("orderNo={}", orderNo);
        orderInfo.setOrderNo(orderNo);
        Date now = new Date(System.currentTimeMillis());
        orderInfo.setCreatedBy("system");
        orderInfo.setCreatedDate(now);
        orderInfo.setLastUpdatedBy("system");
        orderInfo.setLastUpdatedDate(now);
    }


    private ResponseVo setNxLock(OrderInfo orderEntity, Integer flag) {
        String goodCode = orderEntity.getGoodCode();
        Jedis jedis = (Jedis) redisConnectionFactory.getConnection().getNativeConnection();
        // 查库存时加上分布式锁
        String lockKey = "lock_"   goodCode;
        long currentTime = System.currentTimeMillis();
        Long lockResult = jedis.setnx(lockKey, String.valueOf(currentTime));
        if (lockResult == 1) {
            // 设置锁失效时间5s
            try {
                 jedis.expire(lockKey, 5);
                 logger.info("get distribute lock success, lockKey={}", lockKey);
                 return queryStockAndInsertOrder(orderEntity);
             } catch (Exception e) {
                  logger.error("", e);
                  return ResponseVo.error(e.getMessage());
            } finally {
                delLockByExecuteJedisCommand(jedis, lockKey, currentTime, flag);
            }
        } else {
            logger.warn("get redis lock failed, stop to order");
            return ResponseVo.error("请稍后再下单,其他客户正在对同一商品下单");
        }
    }

    /**
     * 查询库存并保存订单
     * @param orderEntity
     * @return
     */
    private ResponseVo queryStockAndInsertOrder(OrderInfo orderEntity) {
        String goodCode = orderEntity.getGoodCode();
        String requestUrl = queryGoodStockServiceUrl   "?goodCode={goodCode}";
        Map<String, Object> paramMap = new HashMap<>(1);
        paramMap.put("goodCode", goodCode);
        // 通过RestTemplate调用远程库存服务
        JSONObject jsonResponse = restTemplate.getForObject(requestUrl, JSONObject.class, paramMap);
        logger.info("queryResponse={}", JSONUtil.toJsonStr(jsonResponse));
        if (jsonResponse == null) {
              return ResponseVo.error("远程调用库存服务失败");
        }
        int status = jsonResponse.getInt("status");
        if (status != 200) {
            return ResponseVo.error(status, jsonResponse.getStr("message"));
        }
        StockInfo stockInfo = jsonResponse.get("data", StockInfo.class);
        if (stockInfo.getCount() <= orderEntity.getGoodCount()) {
            return ResponseVo.error("商品库存不足");
        }
        completeOrderInfo(orderEntity);
        int insertCount = this.baseMapper.insert(orderEntity);
        logger.info("insertCount={}", insertCount);
        // 异步减库存
        asyncDecreaseStock(stockInfo, orderEntity.getGoodCount());
        return ResponseVo.success(orderEntity);
    }

    private ResponseVo redisTemplateLock(OrderInfo orderEntity) {
        String goodCode = orderEntity.getGoodCode();
        String lockKey = "lock_"   goodCode;
        Long value = System.currentTimeMillis();
        // ValueOperation#setIfAbsent(key, value)等同与jedis.setNx(key,value)方法,都可以实现redis不存在key值时的添加缓存
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, String.valueOf(value));
        if (flag) {
            // 加锁成功,执行查库存操作
            try {
                stringRedisTemplate.expire(lockKey, 5, TimeUnit.SECONDS);
                logger.info("get distribute lock success, lockKey={}", lockKey);
                return queryStockAndInsertOrder(orderEntity);
            } catch (Exception e) {
                logger.error("", e);
                return ResponseVo.error(e.getMessage());
            } finally {
                // lua脚本,注意lua脚本语言的语法,Lua小白读者可跳转到这里学习:https://www.runoob.com/lua/lua-tutorial.html
                String script = "local value = redis.call('GET', KEYS[1])n"  
                            "if value == ARGV[1] then n"  
                            "  redis.call('DEL', KEYS[1])"  
                            "return 1 n"  
                            "end "  
                            "return 0 n" ;
                // 构造RedisScript实例
                RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
                List<String> keys = new ArrayList<>(1);
                keys.add(lockKey);
                Long count =  stringRedisTemplate.execute(redisScript, keys, String.valueOf(value));
                if (count == 1) {
                    logger.info("release redis lock success");
                } else {
                    logger.warn("release redis lock failed");
                }

            }
        } else {
            logger.warn("get redis lock failed, stop to order");
            return ResponseVo.error("请稍后再下单,其他客户正在对同一商品下单");
        }

    }


    private void delLockByExecuteJedisCommand(Jedis jedis, String lockKey, Long currentTime, Integer flag) {
        if (flag==1) {
            String value =  jedis.get(lockKey);
            if (value !=null && Long.parseLong(value) == currentTime) {
                jedis.del(lockKey);
                logger.info("release redis lock, lockKey={}",lockKey);
            }
        } else if (flag == 2) {
            delLockByJedisExecuteLuaScript(jedis, lockKey, currentTime);
        }
    }

    private void delLockByJedisExecuteLuaScript(Jedis jedis, String lockKey, Long currentTime) {
        String script = "local value = redis.call('GET', KEYS[1])n"  
                "if value == ARGV[1] then n"  
                "  redis.call('DEL', KEYS[1])"  
                "return 1 n"  
                "end "  
                "return 0 n" ;
        List<String> keys = new ArrayList<>(1);
        keys.add(lockKey);
        List<String> args = new ArrayList<>(1);
        args.add(String.valueOf(currentTime));
        // 注意这里的返回类型必须使用Long,用Integer的话会报错
        Long count = (Long) jedis.eval(script, keys, args);
        if (count == 1) {
            logger.info("release redis lock success");
        } else {
            logger.warn("release redis lock failed");
        }
    }

    /**
     * 异步减库存 为了简化步骤这里使用线程池模拟减库存,真实的电商环境会使用RabbitMq或者RocketMq消息队列来实现减库存的逻辑
     * @param stockInfo
     * @param orderCount
     */
    private void asyncDecreaseStock(StockInfo stockInfo, int orderCount) {
        threadPoolExecutor.execute(() -> {
            // 减库存
            int remainCount = stockInfo.getCount() - orderCount;
            stockInfo.setCount(remainCount);
            stockInfo.setLastUpdatedBy("system");
            stockInfo.setLastUpdatedDate(new Date(System.currentTimeMillis()));
            ResponseVo updateResponse = restTemplate.postForObject(updateStockCountUrl, stockInfo, ResponseVo.class);
            logger.info("updateResponse={}", JSONUtil.toJsonStr(updateResponse));
            if (updateResponse.getStatus() == 200) {
                logger.info("update stock count success");
            } else {
                logger.warn("update stock count failed, stockInfo={}, remainCount={}", stockInfo, remainCount);
            }
        });
    }

}

为了避免一个客户端释放别的客户端持有的锁,在释放锁之前需要进行校验要删除的锁是否是自己加的锁,也叫验签。直接通过Redis客户端先执行get(key)判断value值是否与预期的值相等后再删除key释放锁,这种方式无法保证操作的原子性。因为存在redis验签之后删除key之前突然出现服务宕机的情况,而通过redis执行lua原子脚本的方式恰好保证了操作的原子性。

通过redis客户端执行lua脚本有两种方式,一种是通过Jedis#eval方法执行,另一种是通过RedisTemplate#execute方法实现。通过追踪方法执行链,我们会发现它们的底层其实都是通过RedisConnnection执行eval命令运行行lua脚本的。

5) 控制器层编码

控制器层注意实现创建订单接口参数的接收与服务层的调用

代码语言:javascript复制
package com.spring.cloud.alibabaconsumer.controller;

import com.spring.cloud.alibaba.commons.pojo.OrderInfo;
import com.spring.cloud.alibaba.commons.pojo.ResponseVo;
import com.spring.cloud.alibabaconsumer.service.OrderService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Resource
    private OrderService orderService;

    @PostMapping("/create")
    public ResponseVo createOrder(@RequestBody OrderInfo orderEntity, @RequestParam("flag") Integer flag) {

        return orderService.createOrder(orderEntity, flag);
    }
}

3 效果体验

编码完成之后就是把项目跑起来查看效果 时刻了!

3.1 服务启动

本地启动MysqlRedis服务, Linux服务器上单机模式启动Nacos服务

本机上的MysqlRedis服务可通过“我的电脑”->右键"管理"->“服务和应用程序”->“服务”找到按照在本地的MysqlRedis服务,然后点左上角的“启动”此服务完成MysqlRedis服务的启动。

安装在Linux服务器上的Nacos服务的启动可通过远程ssh客户端连接Linux服务器后进入nacos的bin执行单机模式启动命令(Nacos集群模式在我的1核2G的服务器上使用不同端口代替不同实例启动不了,只好用单机模式了)

代码语言:javascript复制
ssh startup.sh -m standalaone

如果是在自己的windows系统计算机上启动nacos服务,则通过 dos命令在nacos的bin目录下通过输入cmd后在打开的控制台中输入以下命令后回车即可

代码语言:javascript复制
startup.cmd -m standalone

然后在IDEA中先后启动alibaba-service-provideralibaba-service-consumer两个微服务

两个微服务启动成功后我们进入在浏览器中输入以下网址进入nacos的UI界面可以看到stock-serviceorder-service都注册到了Nacos注册中心

3.2 创建订单接口测试

两个微服务启动成功后在postman中调用创建订单接口(可以通过修改flag参数值查看不同的加锁和释放redis锁方式)

代码语言:javascript复制
POST http://localhost:9002/order-service/order/create?flag=2

{
	"userId": 1,
	"goodCode": "iphone_11",
	"goodCount": 10,
	"orderMoney": 6500000
}

点击Send按钮后可以看到接口响应信息如下:

代码语言:javascript复制
{
    "uuid": "c6f638f1-a1d8-4b98-9be7-2508a27f0a3b",
    "status": 200,
    "message": "OK",
    "data": {
        "createdBy": "system",
        "createdDate": "2022-01-03 23:20:43",
        "lastUpdatedBy": "system",
        "lastUpdatedDate": "2022-01-03 23:20:43",
        "orderId": 12,
        "userId": 1,
        "orderNo": "20220103232043.752",
        "goodCode": "iphone_11",
        "goodCount": 10,
        "orderMoney": 6500000
    }
}

alibab-service-consumer服务的控制台中可以看到如下日志信息:

代码语言:javascript复制
2022-01-03 23:20:43.679  INFO 3884 --- [nio-9002-exec-5] c.s.c.a.service.impl.OrderServiceImpl    : get distribute lock success, lockKey=lock_iphone_11
2022-01-03 23:20:43.749  INFO 3884 --- [nio-9002-exec-5] c.s.c.a.service.impl.OrderServiceImpl    : queryResponse={"data":{"unitPrice":650000,"lastUpdatedBy":"system","count":860,"lastUpdatedDate":"2022-01-03 14:26:58","createdDate":"2021-11-08 23:42:02","goodName":"苹果手机11","createdBy":"heshengfu","id":4,"goodCode":"iphone_11"},"message":"OK","uuid":"3c3c4016-ec9f-40c0-928f-c4375c52ea14","status":200}
2022-01-03 23:20:43.753  INFO 3884 --- [nio-9002-exec-5] c.s.c.a.service.impl.OrderServiceImpl    : orderNo=20220103232043.752
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@12906d4] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@21355453 wrapping com.mysql.cj.jdbc.ConnectionImpl@1f928ab] will not be managed by Spring
==>  Preparing: INSERT INTO orders ( user_id, order_no, good_code, good_count, order_money, created_by, created_date, last_updated_by, last_updated_date ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ? ) 
==> Parameters: 1(Long), 20220103232043.752(String), iphone_11(String), 10(Integer), 6500000(Long), system(String), 2022-01-03 23:20:43.753(Timestamp), system(String), 2022-01-03 23:20:43.753(Timestamp)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@12906d4]
2022-01-03 23:20:43.771  INFO 3884 --- [nio-9002-exec-5] c.s.c.a.service.impl.OrderServiceImpl    : insertCount=1
2022-01-03 23:20:43.794  INFO 3884 --- [pool-4-thread-2] c.s.c.a.service.impl.OrderServiceImpl    : updateResponse={"data":1,"message":"OK","uuid":"288b8ca8-a9f2-4dec-9f04-3fb1de1adb6b","status":200}
2022-01-03 23:20:43.794  INFO 3884 --- [pool-4-thread-2] c.s.c.a.service.impl.OrderServiceImpl    : update stock count success
2022-01-03 23:20:43.942  INFO 3884 --- [nio-9002-exec-5] c.s.c.a.service.impl.OrderServiceImpl    : release redis lock success

在上述日志中我们可以清晰地看到获取获取到redis锁和释放redis锁,以及订单表插入数据的sql执行日志。

alibaba-service-consumer服务控制台中可以看到查询库存和减库存的日志信息

代码语言:javascript复制
2022-01-03 23:20:43.695  INFO 19532 --- [nio-9000-exec-4] c.s.c.a.s.p.service.impl.StockService    : goodCode=iphone_11
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a91626] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@7270531 wrapping com.mysql.cj.jdbc.ConnectionImpl@517d56] will not be managed by Spring
==>  Preparing: SELECT id,good_code,good_name,count,unit_price,created_by,created_date,last_updated_by,last_updated_date FROM stock_info WHERE good_code = ? 
==> Parameters: iphone_11(String)
<==    Columns: id, good_code, good_name, count, unit_price, created_by, created_date, last_updated_by, last_updated_date
<==        Row: 4, iphone_11, 苹果手机11, 860, 650000, heshengfu, 2021-11-08 23:42:02, system, 2022-01-03 14:26:58
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@a91626]
2022-01-03 23:20:43.779  INFO 19532 --- [nio-9000-exec-5] c.s.c.a.s.p.service.impl.StockService    : id=4, count=850
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@909c37] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@2391458 wrapping com.mysql.cj.jdbc.ConnectionImpl@517d56] will not be managed by Spring
==>  Preparing: UPDATE stock_info SET good_code=?, good_name=?, count=?, unit_price=?, created_by=?, created_date=?, last_updated_by=?, last_updated_date=? WHERE id=? 
==> Parameters: iphone_11(String), 苹果手机11(String), 850(Integer), 650000(Long), heshengfu(String), 2021-11-08 23:42:02.0(Timestamp), system(String), 2022-01-03 23:20:43.0(Timestamp), 4(Long)
<==    Updates: 1

库存服务控制台中也打印出了查询库存和减库存的详细信息

然后我们查询两个数据库中的stock_info表和orders表都能看到库存数据的改变以及订单数据的增加

5 小结

  • 本文以nacos作为注册中心,搭建了两个微服务模拟电商项目中的库存服务和订单服务,主要演示了分布式场景下使用redis实现分布式事务锁。
  • redis 面向java语言的两种常用的客户端有lettuce和Jedis;
  • redis执行Lua脚本保证了垮库操作事务的原子性,redis执行lua脚本主要有两种方式:Jedis#eval(String script, List<String> keys, List<String> args)RedisTemplate#execute(RedisScript<T> script, List<K> keys, Object... args)
  • 不足之处:没有启动多个alibaba-service-provider服务实例,也没使用Jemter压测工具进行高并发场景测试, 下一篇文章将对多实例和高并发场景进行补充测试

本文项目已上传到gitee个人代码仓库,感兴趣的朋友可以克隆下来参考,gitee代码仓库地址 https://gitee.com/heshengfu1211/alibaba-demos.git

代码语言:javascript复制
             ---END---

 原创不易,欢迎看到这里的朋友都能点亮【在看】,谢谢!

0 人点赞