SpringBoot 实现Zk分布式锁+Redis分布式锁,最新教程,细到极致

2 篇文章 0 订阅
1 篇文章 0 订阅

  各位亲爱的小伙伴们,晚上好呀,我是你们的老朋友,像风一样的男人,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、最后,各位小伙伴们,麻烦给老哥一个点赞、关注、收藏三连好吗,你的支持是老哥更新最大的动力,谢谢!

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jesscia ^_^

您的打赏将是我努力的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值