各位亲爱的小伙伴们,晚上好呀,我是你们的老朋友,像风一样的男人,Jessica老哥,今天,是个不同寻常的日子,相信各位也知道,全国莘莘学子正在为自己的梦想的大学,奋笔执考,那么,Jessica老哥在这里,祝愿各位高考小伙伴,在今明两天超常发挥,在金秋八月金榜题名,在九月步入理想的大学!
说着说着,搞得咱们自己也很激动,哈哈哈。不过咱们作为过来人,还是做好自己的事情再说。
那么,趁着这个时候,我们来正式学习一下,分布式锁
,和各位小伙伴们一样,学这个十分的痛苦,而且这玩意工作中用的很少(中大厂用的很多。小厂单机应用,基本上用不到,别杠),但是呢,我们去面试,面试官又经常喜欢问分布式锁,让人十分的痛苦。
所以,老哥决定站出来,用文字+代码的方式,全面剖析分布式锁,让各位小伙伴爽到不要不要的
闲话不扯了,让我们开始吧。
先预热一下,基础知识
① 什么是分布式锁
②为什么需要分布式锁:
③分布式锁有哪些,对比一下
④Zk分布式锁原理是什么
⑤Redis分布式锁的原理是什么
1、接着,我们讲讲Zk分布式锁代码实现,引入 Curator依赖,借助 Curator+Zk来实现分布式锁, Curator是Apache ZooKeeper的Java客户端库
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
application.yml
server:
port: 8080
zookeeper:
connect: 101.35.245.191:2181
2、然后Curator有五种锁方案,我们这次选择第二种
3、新建一个com.dowhere java包,然后新建util包
在util 新建一个类CuratorClientUtil
@Component
@Slf4j
public class CuratorClientUtil implements DisposableBean {
@Value("${zookeeper.connect}")
private String zookeeperConnect;
/**
* 销毁需要用到get方法
*/
@Getter
private CuratorFramework client;
@PostConstruct
public void init() {
this.client = CuratorFrameworkFactory.builder().
connectString(zookeeperConnect).
sessionTimeoutMs(60 * 1000).
connectionTimeoutMs(15 * 1000).
retryPolicy(new ExponentialBackoffRetry(3000, 10))
.build();
this.client.start();
}
@Override
public void destroy() throws Exception {
try {
if (Objects.nonNull(getClient())) {
getClient().close();
}
} catch (Exception e) {
log.info("CuratorFramework close error=>{}", e.getMessage());
}
}
}
3A、新建controller, 新建一个类 ZkLockController
@RestController
@Slf4j
public class ZkLockController {
/**
* 卖10张票,要求全部卖出,不能超卖
*/
private int tickets = 10;
@Autowired
private CuratorClientUtil curatorClientUtil;
@GetMapping("/zkLock")
public Object testLock() throws Exception {
String threadName = Thread.currentThread().getName();
InterProcessMutex mutex = new InterProcessMutex(curatorClientUtil.getClient(), "/lock");
try {
//尝试获取锁,最长等待3s,超时放弃获取
boolean lockFlag = mutex.acquire(3000, TimeUnit.SECONDS);
//获取锁成功
if (lockFlag) {
log.info("当前的票数为: {}",tickets);
Thread.sleep(100);
tickets --;
} else {
log.info("{}---获取锁fail", threadName);
}
} catch (Exception e) {
log.info("{}---获取锁异常", threadName);
} finally {
//释放锁
mutex.release();
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("data","线程: "+threadName+"执行完成");
return jsonObject.toString();
}
}
5、然后开始测试,这次我们使用的工具是全新一代测试工具—apifox,apifox下载官网,这玩意最近很火,老哥专门学了一下,然后我简单介绍一下操作步骤,如果我们是普通接口测试,只需要在接口管理添加测试就可以了
6、如果我们需要并发测试,点击下面的自动化测试,然后点击右上方+号,添加测试名称,就是一个文件夹
7、点进去以后,添加你的测试名称,然后添加步骤,就是你要测试哪个接口,然后选择环境,循环次数,并发线程数,最后运行完事
8、apifox有一点美中不足的是,需要返回的数据是Json,否则执行用例是失败的,不知道哪里可以设置一下,如果有老弟知道,可以在下方留言告诉老哥
9、然后我们需要确认一下,我们的zk是否正常运行
[root@VM-4-2-centos bin]# pwd
/opt/kafka/zookeeper/zookeeper-3.6.3/bin
[root@VM-4-2-centos bin]# ./zkCli.sh
10、新建一个节点,我们在代码里用得是/lock节点
[zk: localhost:2181(CONNECTED) 1] create /lock
[zk: localhost:2181(CONNECTED) 2] ls /
[Jessica, admin, app1, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, lock, lock1, log_dir_event_notification, zookeeper]
11、然后点击运行apifox测试
查看IDEA控制台:没有问题,Zk分布式锁结束
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
12、接下来是我们的Redis分布式锁,从上面的图,我们已经了解到,实现Redis分布式锁有三种解决方案
第一种,使用Setnx、超时机制获取锁+Lua脚本释放锁,性能还可以,但是呢,有缺点,不支持可重入,不支持可重试
我们来分析下为什么我们自定义的分布式锁无法实现可重入。
首先定义两个方法,method1 和 method2,method1 在获取到锁的情况下,紧接着去调用 method2 方法,而在 method2 方法中又一次获取了锁,method1 和 method2 是在同一个线程内,在同一个线程内重复获取锁,这就是锁的重入。
method1 在获取锁的时候,会执行命令SET lock thread1 NX EX 10,在获取到锁后,method1 调用了 method2,接着 method2 尝试获取锁,再一次执行命令SET lock thread1 NX EX 10,根据 SETNX 的特性,由于 method1 已经 set 成功,那么 method2 肯定会设置失败,所以也就没办法实现重入。
第二种,使用Redis自带的WatchDog机制,实现了可重入,但是呢,代码实现起来比较复杂,还需要搭建分片集群,实现高可用,当然了,老哥也有分片集群的文章,Redis 搭建分片集群
第三种,使用Redisson,实现分布式锁,这种和第二种一样,也需要搭建分片集群,但是,代码实现起来稍微简单一点。然后,老哥推荐大家去看一下人家github的文档,Redisson 官方Github文档
13、接下来,老哥重点讲下Redisson实现原理,这个原理和ReentrantLock的可重入锁原理是一样的
下图是加入了可重入
我们可以在获取锁的时候,首先判断锁是否已经被占用,如果已经被占用,判断是否是当前线程所占用的,如果是同一个线程占用,也会让其获取到锁,但是会额外增加一个计数器,用于记录重入的次数,即当前线程总共获取了几次锁。当前线程每获取一次锁,计数器便加1,而在释放锁时,每释放一次锁,计数器就会减1,直至计数器为 0 时,将当前锁直接删除。那么现在,我们不仅要在锁中记录获取锁的线程,还要记录当前线程重入次数,即获取了几次锁,显然,String 类型的数据结构并不满足这个业务需求,这里可以使用 Hash 类型进行存储,这样就可以在 key 的位置,记录锁的名称,在 field 的位置记录线程标识,在 value 的位置记录锁重入次数。
下图是除了可重入,利用WatchDog加入可重试
14、然后的话,为了更加清晰的模拟Redis分布式锁,我们需要创建两个一样的maven工程,端口号不一样,然后我们先新建一个maven工程,命名redislock-demo-1
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.20</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
application.yml 注意,第二个模块只需要改端口8082
server:
port: 8081
spring:
application:
name: redislock-demo-1
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/redisdemo?useSSL=false&serverTimezone=UTC
username: root
password: root
redis:
host: 101.34.7.236
port: 6379
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
jackson:
default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
type-aliases-package: com.dowhere.pojo # 别名扫描包
logging:
level:
com.dowhere: debug
15、新建 com.dowhere包,下面新建config
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String redisConnectIp;
@Value("${spring.redis.port}")
private String redisConnectPort;
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://"+redisConnectIp+":"+redisConnectPort);
// 创建RedissonClient对象
return Redisson.create(config);
}
}
16、新建controller 、service、serviceImpl mapper
@RestController
public class RedisLockController {
@Autowired
private RedisLockService redisLockService;
@PostMapping("/seckill/{id}/{user}")
public boolean secKill(@PathVariable Long id,@PathVariable String user){
return redisLockService.updateStock(id,user);
}
}
public interface RedisLockService {
boolean updateStock(Long id,String user);
}
@Service
public class RedisLockServiceImpl extends ServiceImpl<StockMapper,Stock> implements RedisLockService {
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateStock(Long id,String user) {
synchronized (user.intern()){
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.eq(User::getUser, user);
User count = userMapper.selectOne(userLambdaQueryWrapper);
if (count !=null){
return false;
}
//1、更新库存
//等同于 update set stock = stock - 1 where id=#{id} and stock > 0
this.update().setSql("stock = stock - 1")
.eq("id", id).gt("stock", 0)
.update();
//2、模拟业务超时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e.getMessage());
}
//3、插入订单
User userOrder = new User();
userOrder.setId(Long.valueOf(RandomUtil.randomNumbers(6)));
userOrder.setUser(user);
userMapper.insert(userOrder);
}
return true;
}
}
@Mapper
public interface StockMapper extends BaseMapper<Stock> {
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
17、然后是实体类 新建pojo包,接着新建第二个模块 redislock-demo-2,重复以上步骤
@Data
@TableName("tb_test")
public class Stock {
@TableId
private Long id;
private Integer stock;
}
@Data
@TableName("tb_test_user")
public class User {
@TableId
private Long id;
private String user;
}
18、然后我们使用mysql,新建一个数据库,redisdemo,新建两张表,tb_test插入一条数据,表示当前库存
CREATE TABLE `tb_test` (
`id` bigint NOT NULL,
`stock` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `tb_test_user` (
`id` bigint NOT NULL,
`user` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
19、然后的话,我是这样想的,使用Nginx 负载均衡,默认轮询,一台一个请求, 先模拟synchronized 锁失效的情况,然后在使用Redisson 解决问题
修改本地Nginx conf/nginx.conf配置,有些小伙伴们可能看不太懂,可以看我之前的Nginx搭建,Linux 安装Nginx,同时配置Https,里面有介绍,这里大概简单说下,将/api 替换成 /,然后轮询转发到相应的机器(一个请求到一台机器)
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/json;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
# 指定前端项目所在的位置
location / {
root html/hmdp;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location /api {
default_type application/json;
#internal;
keepalive_timeout 30s;
keepalive_requests 1000;
#支持keep-alive
proxy_http_version 1.1;
rewrite /api(/.*) $1 break;
proxy_pass_request_headers on;
#more_clear_input_headers Accept-Encoding;
proxy_next_upstream error timeout;
#单机
#proxy_pass http://127.0.0.1:8081;
#模拟分布式
proxy_pass http://backend;
}
}
upstream backend {
server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;
server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;
}
}
20、windows 打开 nginx文件夹,然后输入start nginx.exe启动
21、然后IDEA打断点,启动,记得第二个模块打断点
22、新建两个测试请求,http://localhost:8080/api/seckill/1/Jessica
23、断点提示,两个断点都没有值
24、快速F9放行,然后查看数据库,可以看到,synchronized锁无法控制不同JVM的线程访问
25、修改两个模块的业务层代码
@Autowired
private RedissonClient redissonClient;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateStock(Long id,String user) {
//1.1 使用Redssion分布式锁
RLock lock = redissonClient.getLock("Redssion:lock:stock:" + id);
try {
//1.2 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1,10, TimeUnit.SECONDS);
if (isLock){
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.eq(User::getUser, user);
User count = userMapper.selectOne(userLambdaQueryWrapper);
if (count !=null){
return false;
}
//1、更新库存
//等同于 update set stock = stock - 1 where id=#{id} and stock > 0
this.update().setSql("stock = stock - 1")
.eq("id", id).gt("stock", 0)
.update();
//2、插入订单
User userOrder = new User();
userOrder.setId(Long.valueOf(RandomUtil.randomNumbers(6)));
userOrder.setUser(user);
userMapper.insert(userOrder);
}
} catch (InterruptedException e) {
throw new RuntimeException(e.getMessage());
} finally {
lock.unlock();
}
return true;
}
26、然后发起测试,可以看到,有个线程无法获取到锁
27、查看数据库,没有问题,结束
28、最后,各位小伙伴们,麻烦给老哥一个点赞、关注、收藏三连好吗,你的支持是老哥更新最大的动力,谢谢!