我们知道同一个进程里面为了解决资源共享而不出现高并发的问题可以通过高并发编程解决,通过给变量添加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脚本来实现。另两种方式笔者有时间了再来另外通过实战的方式撰文讲解。
redis之所以能实现分布式事务锁是因为它是一个全局数据库,而且它是一个key-value形式的NO-SQL数据库,对于不同jvm进程中的多线程执行同一段代码时可以实现全局加锁和释放锁操作。setnx命令是判断redis缓存中是否有这个key, 没有才set成功,set成功表示拿到了分布式锁,可以进行后面需要控制并发访问的逻辑。为了防止加锁的机器宕机造成的死锁问题可以通过redis对缓存key 设置过期时间来解决;而执行lua脚本是一个原子操作,同一时间只能有一个客户端在执行,这对于保证分布式高并发场景下事务的原子性和一致性是非常必要的。因此通过执行lua脚本实现分布式事务锁就成为了一个非常好的解决方案。
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命令用法。
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。
在我的上一篇有关微服务实践的文章记一次使用Nacos 2.0.3版本搭建微服务注册中心和客户端的踩坑填坑详细过程项目的基础上搭建微服务聚合项目alibaba-demos。增加三个子模块项目:alibaba-commons
(公共模块项目), alibaba-service-provider
(微服务提供者模块项目)及alibaba-service-consumer
(微服务消费者模块项目)。
alibaba-service-provider
项目模拟电商库存服务,alibaba-service-consumer
项目模拟电商订单服务,两个微服务均对外提供web服务。
alibaba-demos
项目pom.xml
文件
<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.0modelVersion>
<groupId>com.spring.cloudgroupId>
<artifactId>alibaba-demosartifactId>
<version>1.0-SNAPSHOTversion>
<modules>
<module>alibaba-commonsmodule>
<module>alibaba-service-providermodule>
<module>alibaba-service-consumermodule>
modules>
<name>alibaba-demosname>
<description>spring cloud alibaba demosdescription>
<packaging>pompackaging>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.2.7.RELEASEspring-boot.version>
<spring-cloud.version>Hoxton.RELEASEspring-cloud.version>
<spring-cloud-alibaba.version>2.2.2.RELEASEspring-cloud-alibaba.version>
properties>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.7.RELEASEversion>
<relativePath/>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-bootartifactId>
exclusion>
<exclusion>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-testartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
<version>2.0.3version>
dependency>
<dependency>
<groupId>com.alibaba.springgroupId>
<artifactId>spring-context-supportartifactId>
<version>1.0.2version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-consul-discoveryartifactId>
<version>2.2.0.RELEASEversion>
<exclusions>
<exclusion>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-consul-discoveryartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
exclusion>
<exclusion>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
<version>${spring-cloud-alibaba.version}version>
dependency>
<dependency>
<groupId>org.reflectionsgroupId>
<artifactId>reflectionsartifactId>
<version>0.9.10version>
dependency>
<dependency>
<groupId>io.prometheusgroupId>
<artifactId>simpleclientartifactId>
<version>0.0.9version>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.4version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.20version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.16.18version>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
plugins>
build>
project>
alibaba-commons
模块项目pom.xml
<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-demosartifactId>
<groupId>com.spring.cloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>alibaba-commonsartifactId>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.4.5version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.5version>
dependency>
dependencies>
project>
alibaba-service-provider
模块项目pom.xml
<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-demosartifactId>
<groupId>com.spring.cloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>alibaba-service-providerartifactId>
<dependencies>
<dependency>
<groupId>com.spring.cloudgroupId>
<artifactId>alibaba-commonsartifactId>
<version>1.0-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
exclusion>
<exclusion>
<groupId>com.alibaba.springgroupId>
<artifactId>spring-context-supportartifactId>
exclusion>
<exclusion>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
exclusion>
<exclusion>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
exclusion>
<exclusion>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-consul-discoveryartifactId>
exclusion>
<exclusion>
<groupId>org.reflectionsgroupId>
<artifactId>reflectionsartifactId>
exclusion>
<exclusion>
<groupId>io.prometheusgroupId>
<artifactId>simpleclientartifactId>
exclusion>
<exclusion>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
exclusion>
<exclusion>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.3.2.RELEASEversion>
<configuration>
<mainClass>mainClass>
configuration>
<executions>
<execution>
<id>repackageid>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
aliba-service-consumer
模块项目pom.xml
<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.0modelVersion>
<groupId>com.spring.cloudgroupId>
<artifactId>alibaba-consumerartifactId>
<version>1.0.0-SNAPSHOTversion>
<name>alibaba-consumername>
<description>Demo project for Spring Bootdescription>
<parent>
<artifactId>alibaba-demosartifactId>
<groupId>com.spring.cloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-bootartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
exclusion>
<exclusion>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
exclusion>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-keyvalueartifactId>
<version>2.5.6version>
dependency>
<dependency>
<groupId>com.spring.cloudgroupId>
<artifactId>alibaba-commonsartifactId>
<version>1.0-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
exclusion>
<exclusion>
<groupId>com.alibaba.springgroupId>
<artifactId>spring-context-supportartifactId>
exclusion>
<exclusion>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
exclusion>
<exclusion>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-consul-discoveryartifactId>
exclusion>
<exclusion>
<groupId>org.reflectionsgroupId>
<artifactId>reflectionsartifactId>
exclusion>
<exclusion>
<groupId>io.prometheusgroupId>
<artifactId>simpleclientartifactId>
exclusion>
<exclusion>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
exclusion>
<exclusion>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
exclusion>
<exclusion>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
exclusion>
<exclusion>
<groupId>org.xmlunitgroupId>
<artifactId>xmlunit-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
<version>${spring-cloud-alibaba.version}version>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>org.xmlunitgroupId>
<artifactId>xmlunit-coreartifactId>
<version>2.6.2version>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.2.7.RELEASEversion>
<configuration>
<mainClass>com.spring.cloud.alibabaconsumer.AlibabaConsumerApplicationmainClass>
configuration>
<executions>
<execution>
<id>repackageid>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
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-provider
与alibaba-service-consumer
及nacos注册中心之间的关系。
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');
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")
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;
}
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;
}
}
这里我们选用MybatisPlus
作为持久层框架,通过继承BaseMapper可直接获得基本的数据库CRUD方法。
@Repository
public interface StockMapper extends BaseMapper<StockInfo> {
}
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;
}
}
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);
}
}
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
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;
}
}
@Repository
public interface OrderMapper extends BaseMapper<OrderInfo> {
}
服务层主要实现创建订单方法
订单服务接口类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);
}
}
编码完成之后就是把项目跑起来测试功能时刻了!
本地启动Mysql
和Redis服务
, Linux
服务器上单机模式启动Nacos
服务
本机上的Mysql
和Redis
服务可通过我的电脑->右键->管理->服务和应用程序->服务找到按照在本地的Mysql
和Redis
服务,然后点左上角的“启动”此服务完成Mysql
和Redis
服务的启动。
安装在Linux
服务器上的Nacos
服务的启动可通过远程ssh客户端连接Linux
服务器后进入nacos
的bin
目录执行单机模式启动命令(Nacos集群模式在我的1核2G的服务器上使用不同端口代替不同实例启动不了,只好用单机模式了)
ssh startup.sh -m standalaone
如果是在自己的windows系统计算机上启动nacos服务,则通过 dos命令在nacos
的bin
目录下通过输入cmd
后在打开的控制台中输入以下命令后回车即可
startup.cmd -m standalone
然后在IDEA中先后启动alibaba-service-provider
和alibaba-service-consumer
两个微服务
两个微服务启动成功后我们进入在浏览器中输入以下网址进入nacos
的UI界面可以看到stock-service
和order-service
都注册到了nacos
注册中心
两个微服务启动成功后在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
表都能看到库存数据的改变以及订单数据的增加
本文以nacos作为注册中心,搭建了两个微服务模拟电商项目中的库存服务和订单服务,主要演示了分布式场景下使用redis实现分布式事务锁。
redis 面向java语言的两种常用的客户端有lettuce和Jedis;
redis执行Lua脚本保证了垮库操作事务的原子性,redis执行lua脚本主要有两种方式:Jedis#eval(String script, List
和RedisTemplate#execute(RedisScript
不足之处:没有启动多个alibaba-service-provider
服务实例,也没使用Jemter压测工具进行高并发场景测试, 下一篇文章将对多实例和高并发场景进行补充测试。
本文首发个人微信号【阿福谈Web编程】,欢迎CSDN上的粉丝朋友加个微信公众号关注,实践过程有什么疑难问题可通过微信公众号中的菜单【作者联系方式】加我微信向我提问,笔者看到消息后会尽力帮助解决。让我们一起在编程的路上一起成长为一名大牛!
注意:需要本文项目源码的小伙伴可通过关注我的个人微信公众号【阿福谈Web编程】,在消息对话框中输入关键字【alibaba-demos】获取gitee代码仓库地址