Spring Cloud 微服务系列文章,点击上方合集↑
1. 开头
在单体应用中,我们可以用Java的synchronized
或lock
来使用锁,但在微服务的场景下,一个应用会部署多个实例,就需要保证多个实例的多个线程同时只能有一个线程来操作资源,那就需要分布式锁,
Redisson分布式锁的基本原理是通过Redis的setnx
命令实现的。当一个进程需要获取锁时,通过调用Redis的setnx
命令,在Redis中创建一个key
表示锁的名称,如果成功地创建了这个
key
,则表示获取锁成功,可以执行相应的业务逻辑。如果创建key
失败,则表示锁已经被其他进程获取了,当前进程需要等待直到获取到锁为止。当执行完业务逻辑后,需要释放锁,即通过Redis
的del
命令删除锁的key
,这样其他进程就可以获取到锁了。
举个通俗的栗子:你上厕所的时候推一下门看看里面有没有人(尝试获取锁),里面有人你需要在门口等着(等待锁),当他上完后开门了(释放锁),你进去的时候把门关上了(上锁),你执行完脱裤子、拉屎、提裤子等操作后把门打开了(释放锁)。
本文我们将通过抢购茅台的例子来进行演示。
抢购茅台分为三个步骤:
2. 安装运行Redis
Redisson
是基于Redis
的,我们需要先下载运行Redis
。
Redis
是内存数据库,Redisson
= Redis
+ son
(儿子)。
2.1 Windows
-
github下载地址:github.com/redis-windo…
-
github下载很慢,网盘下载(推荐):
「Redis-7.0.8-Windows-x64.zip」来自UC网盘分享
drive.uc.cn/s/4087d341f…
启动Redis
# 启动服务
redis-server.exe redis.conf
# 老版本的配置文件名称
redis-server.exe redis.windows.conf
- 需要指定
redis.conf
配置文件,可以在redis.conf
文件中设置端口、密码等。
进入Redis命令行界面
# redis命令行界面
redis-cli.exe
# 设置键值对
set mykey hello
# 获取值
get mykey
2.2 Linux
以CentOS为例,通过yum
命令安装。
# 安装
sudo yum install redis
# 启动
sudo systemctl start redis
# 进入redis命令行界面
redis-cli
# 设置键值对
set mykey hello
# 获取值
get mykey
3. 业务SQL脚本
执行如下SQL脚本
- 生成商品表
product
- 生成商品订单表
product_order
- 商品表插入一条记录,贵州茅台总数量100瓶
CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '商品名称',
`number` int(11) NOT NULL DEFAULT 0 COMMENT '商品数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `product_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`product_id` bigint(20) DEFAULT NULL COMMENT '商品id',
`number` int(11) DEFAULT NULL COMMENT '数量',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `product` VALUES (1, '贵州茅台', 100);
4. SpringBoot 集成 Redisson
4.1 pom.xml
添加redisson-spring-boot-starter
包依赖
org.redisson
redisson-spring-boot-starter
3.16.8
4.2 application.properties
redis相关配置
# redis 相关
spring.redis.host=localhost
spring.redis.port=6379
4.3 buy()
购买商品的方法
- 判断库存数量是否充足
- 创建订单
- 扣减库存数量
@Resource
private RedissonClient redissonClient;
@Override
public Boolean buy(Long productId, Integer number) {
RLock lock = redissonClient.getLock("lock_key_" + productId);
try {
boolean lockRes = lock.tryLock(5, TimeUnit.SECONDS);
if (!lockRes) {
throw new RuntimeException("获取锁失败~");
}
Product product = productService.getById(productId);
log.info("库存数量:{}", product.getNumber());
// ①判断库存是否充足
if (product.getNumber() < number) {
throw new RuntimeException("库存不足");
}
// ②创建订单
ProductOrder order = new ProductOrder();
order.setProductId(productId);
order.setNumber(1);
order.setCreateTime(LocalDateTime.now());
save(order);
log.info("创建订单:{}", order);
// ③减库存
product.setNumber(product.getNumber() - 1);
productService.updateById(product);
log.info("减库存:{}", product);
} catch (InterruptedException e) {
throw new RuntimeException("出现异常啦~");
} finally {
// 释放锁
lock.unlock();
}
return true;
}
"lock_key_" + productId
这里对商品id加锁,同时只能有一个请求线程操作这个商品,其它请求线程必须等待。- 对商品id加锁表明每一种商品都是独立加锁的,就相当于你上厕所的时候是把那个坑位的门给关了,而不是把卫生间的门给关了。
lock.tryLock(5, TimeUnit.SECONDS)
,这里设置加锁的时间为5秒,如果当前请求线程5秒内还没有执行完操作就自动释放锁,让下一个线程来进行操作。
4.4 加锁与不加锁分析
不加锁会出现什么问题?
假设库存还剩最后1瓶茅台,用户A和用户B同时发起购买1瓶茅台请求,用户A的请求线程判断库存充足,但还没有执行完创建订单和减库存操作(操作需要访问数据库,比较耗时)。此时用户B的请求线程判断库存数量为1,库存也充足,也进入了创建订单和减库存操作,最后创建了两个订单,库存减了两次。
我们对商品加锁后,当用户A的请求线程执行判断库存、创建订单、减库存的购买操作过程中,用户B的请求线程需要等待用户A把这一系列操作做完释放了锁之后才能去执行。
5. 测试
接口地址:http://localhost:8100/product/buy
用JMeter创建10个线程模拟多用户同时循环请求接口,当不加锁的情况下,如下是真实测试的结果:100瓶茅台产生了204个订单。
当加锁后,100瓶茅台正常产生100个订单。
JMeter的使用可以看前面Sentinel 流量控制和接口防护
那一篇文章。
6. 结语
这里通过简单的抢购茅台的例子来演示锁,Redisson分布式锁可以用于单体或者微服务,它是借助redis中间件来加锁,如上内容我们可以创建多个微服务实例,然后调用接口结果也是一样的。
记住:在限时抢购活动中,大量用户通过网络同时请求购买同一个商品,可能导致系统出现并发抢购的情况,从而会导致商品售罄或者超卖等问题。这种情况下一定要用锁,不管是单体应用还是微服务。
Spring Cloud 微服务系列 完整的代码在仓库的sourcecode/spring-cloud-demo
目录下。
gitee(推荐):gitee.com/cunzaizhe/x…
github:github.com/tigerleeli/…
关注微信公众号:“小虎哥的技术博客”,让我们一起成为更优秀的程序员❤️!