引言
我们知道同一个进程里面为了解决资源共享而不出现高并发的问题可以通过高并发编程解决,通过给变量添加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
包下的两个抽象接口RedisConnection
和RedisConnectionFactory
用于获得于Redis
交互的工作连接。Jedis
和Lettuce
两个类库提供了RedisConnectionFactory
接口的实现类LettuceConnectionFactory
和JedisConnectionFactory
。
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
连接对象,并自动导入LettuceConnectionConfiguration
和JedisConnectionConfiguration
两个配置类。同时在项目中缺失两个bean的情况情况下,向Spring IOC
容器中实例化并注入RedisTemplate
和StringRedisTemplate
两个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依赖
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>
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>
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>
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.3 项目配置类
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=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
文件
# 应用服务 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
库存服务与订单服务的关系及创建订单的流程,笔者画了一幅如下所示的简单流程图
2.4 数据库建表与创建实体类
- 新建库存表并添加数据
打开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`),
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');
- 同样打开
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;
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")
private int 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.5 库存微服务编码
- 启动类
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
注解用于开启微服务自动发现功能
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;
}
}
3) Mapper层编码
这里我们选用MybatisPlus
作为持久层框架,通过继承BaseMapper
可直接获得基本的数据库CRUD方法。
@Repository
public interface StockMapper extends BaseMapper<StockInfo> {
}
- Service层编码
public interface IStockService extends IService<StockInfo> {
/**
* 通过商品编码查找库存
*/
ResponseVo findStockByGoodCode(String goodCode);
/**
* 修改库存
*/
ResponseVo updateStockById(StockInfo stockInfo);
}
- 控制器层编码
@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
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.spring.cloud.alibabaconsumer.mapper")
public class AlibabaConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(AlibabaConsumerApplication.class, args);
}
}
- 配置类
RestTemplateConfig
类用于构造实现http
或https
协议的远程服务调用的RestTemplate
模板工具类bean。
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){
return restTemplateBuilder.build();
}
}
TaskPoolConfig
类用于构造自定义线程池,用户下单成功后异步见库存
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;
}
}
- 持久层编码
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> {
}
- 服务层编码
服务层主要实现创建订单方法
代码语言: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 服务启动
本地启动Mysql
和Redis
服务, Linux服务器上单机模式启动Nacos
服务
本机上的Mysql
和Redis
服务可通过“我的电脑”->右键"管理"->“服务和应用程序”->“服务”找到按照在本地的Mysql
和Redis
服务,然后点左上角的“启动”此服务完成Mysql
和Redis
服务的启动。
安装在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-provider
和alibaba-service-consumer
两个微服务
两个微服务启动成功后我们进入在浏览器中输入以下网址进入nacos的UI界面可以看到stock-service
和order-service
都注册到了Nacos注册中心
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按钮后可以看到接口响应信息如下:
代码语言: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
服务的控制台中可以看到如下日志信息:
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
表都能看到库存数据的改变以及订单数据的增加
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---
原创不易,欢迎看到这里的朋友都能点亮【在看】,谢谢!