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

引言

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

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

这个时候就需要引入分布式事务锁方案来解决问题了,分布式事务锁主要有redis、zookeeper和数据库版本锁(也叫乐观锁)三种常用的实现方式。其中以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 通过 Jedis和Lettuce 两个Java开源类库与Redis集成, 无论使用哪种客户端,你要用到spring-data-redis jar包中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类,这个自动配置类的源码如下:

@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.2 微服务项目骨架搭建

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

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

2.2.1 项目maven依赖
  1. alibaba-demos项目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>

    <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
<?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
<?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
<?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.2.2 项目配置类

1 alibaba-service-provider项目application.properties

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

# 数据源配置
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,将库存微服务注册到注册中心

spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=134.175.187.61:8848
spring.cloud.nacos.discovery.namespace=public
spring.cloud.nacos.config.server-addr=134.175.187.61:8848
spring.application.name=stock-service

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

# 应用服务 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

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

库存服务与订单服务的关系及创建订单的流程,笔者画了一幅如下所示的简单流程图,希望能帮助读者朋友更好的理解alibaba-service-provideralibaba-service-consumer及nacos注册中心之间的关系。
mocro_service

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

2.3.1 新建库存表并添加数据

  1. 打开navicat客户端新建连接,使用root账户和登录密码连接本地MySQL服务test数据库后在控制台中执行以下sql脚本
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`),
  UNIQUEKEY`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');
INSERTI NTO`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数据库后执行订单表的创建脚本
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

@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

@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")
    privateint goodCount;

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

BaseEntity.java

@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.4 库存微服务编码

  1. 启动类ServiceProviderApplication.java
@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分页配置类
@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;
     }
}
  1. 持久层编码

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

@Repository
public interface StockMapper extends BaseMapper<StockInfo> {
    
    

}
  1. Service层编码

库存服务接口类IStockService.java

public interface IStockService extends IService<StockInfo> {
    
    
    /**
    * 通过商品编码查找库存
    */
    ResponseVo findStockByGoodCode(String goodCode);
    /**
    * 修改库存
    */
    ResponseVo  updateStockById(StockInfo stockInfo);
}

库存服务实现类StockService.java

@Service
@Slf4j
public class StockService extends ServiceImpl<StockMapper, StockInfo> implements IStockService {
    
    

    private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

    @Override
    public ResponseVo findStockByGoodCode(String goodCode) {
    
    
        log.info("goodCode={}", goodCode);
        QueryWrapper<StockInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("good_code", goodCode);
        StockInfo stockInfo = this.baseMapper.selectOne(queryWrapper);
        ResponseVo responseVo = ResponseVo.success(stockInfo);
        return responseVo;
    }

    @Override
    public ResponseVo updateStockById(StockInfo stockInfo) {
    
    
        log.info("id={}, count={}", stockInfo.getId(), stockInfo.getCount());
        if (StringUtils.isEmpty(stockInfo.getLastUpdatedBy())) {
    
    
            stockInfo.setLastUpdatedBy("system");
            stockInfo.setLastUpdatedDate(new Date(System.currentTimeMillis()));
        }
        Integer updateCount = this.baseMapper.updateById(stockInfo);
        ResponseVo responseVo = ResponseVo.success(updateCount);
        return responseVo;
    }
}
  1. Controller编码
@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)) {
    
    
            thrownew 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){
    
    
            thrownew IllegalArgumentException("parameter id cannot small than 0");
        }
        if(stockInfo.getCount() < 0) {
    
    
            thrownew IllegalArgumentException("parameter count cannot small than 0");
        }
        return stockService.updateStockById(stockInfo);
    }
}

2.5 订单微服务编码

1) 启动类AlibabaConsumerApplication.java

@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类用于构造实现http或https协议的远程服务调用的RestTemplate模板工具类bean。

@Configuration
publicclass RestTemplateConfig {
    
    

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

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

@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. 持久层编码
@Repository
public interface OrderMapper extends BaseMapper<OrderInfo> {
    
    

}
  1. 服务层编码

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

订单服务接口类OrderServic.java

public interface OrderService extends IService<OrderInfo> {
    
    

    ResponseVo createOrder(OrderInfo orderEntity, Integer flag);
}

订单服务实现类OrderServiceImpl.java

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

    privatefinalstatic Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);

    privatefinalstatic 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);
            }
        } elseif (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)Controller编码

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

@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服务器后进入nacosbin目录执行单机模式启动命令(Nacos集群模式在我的1核2G的服务器上使用不同端口代替不同实例启动不了,只好用单机模式了)

ssh startup.sh -m standalaone

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

startup.cmd -m standalone

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

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

3.2 创建订单接口测试

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

POST http://localhost:9002/order-service/order/create?flag=2

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

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

{
    
    
    "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服务的控制台中可以看到如下日志信息:

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服务控制台中可以看到查询库存和减库存的日志信息

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表都能看到库存数据的改变以及订单数据的增加

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压测工具进行高并发场景测试, 下一篇文章将对多实例和高并发场景进行补充测试。

本文首发个人微信号【阿福谈Web编程】,欢迎CSDN上的粉丝朋友加个微信公众号关注,实践过程有什么疑难问题可通过微信公众号中的菜单【作者联系方式】加我微信向我提问,笔者看到消息后会尽力帮助解决。让我们一起在编程的路上一起成长为一名大牛!

注意:需要本文项目源码的小伙伴可通过关注我的个人微信公众号【阿福谈Web编程】,在消息对话框中输入关键字【alibaba-demos】获取gitee代码仓库地址

猜你喜欢

转载自blog.csdn.net/heshengfu1211/article/details/122334488