ITPub博客

首页 > 数据库 > 数据库开发技术 > 如何用Zookeeper实现分布式锁,这篇文章告诉你!

如何用Zookeeper实现分布式锁,这篇文章告诉你!

原创 数据库开发技术 作者:Finally_m 时间:2023-02-21 08:03:00 0 删除 编辑
点击上方蓝字关注我们

一. 分布式锁概念

当我们在单机环境下进行应用开发,涉及到并发同步时,说到如何保证线程的安全,相信大家一定能想到采用synchronized或者Lock的方式,来解决多线程间的代码同步问题。这时多线程的运行都是在同一个JVM之下,我们只要保证锁对象的唯一性,就不会产生任何问题。

关注Java架构栈,干货天天都不断!

但当我们的应用是分布式集群环境,此时就属于多JVM下的工作环境,跨越了多个JVM,不同进程中的锁对象是不一样的,此时就无法通过多线程的锁来解决同步问题。

那么在这种环境下,我们就需要给大家介绍一种更加高级的锁机制,来处理种跨机器多进程之间的数据同步问题——这就是分布式锁。

二. zookeeper分布式锁的实现原理

为了方便大家理解,我给大家梳理了zookeeper实现分布式锁的核心原理,其核心思想就是:

当客户端要获取锁,则创建节点,使用完锁或者客户端异常退出,则删除该节点。接下来我们再把该思想细分出多个具体的实现步骤,如下:

  1. 客户端获取锁时,在lock节点下创建临时顺序 + 监听节点;

  2. 获取lock下面的所有子节点。客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除;

  3. 如果发现自己创建的节点,并非是lock节点下所有子节点中最小的,则说明自己还没有获取到锁。此时客户端需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件;

  4. 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知。此时再次判断自己创建的节点,是否是lock子节点中序号最小的。如果是,则获取到了锁,如果不是,则重复以上步骤继续获取,直到比自己小的一个节点并注册监听。

创建临时节点的好处是,当客户端因为异常挂掉或者正常执行业务关闭后,这个节点会自动删除,不存在其他进程获取不到锁的问题。而顺序节点的好处是,当所有进程过来加锁后,可以利用顺序号进行排队。所以后续节点只会监听它的上一个节点,而不是他们的父节点。如果监听的是父节点,某个节点删除,则其他所有的节点都会参与抢锁操作,这种现象我们称之为“惊群效应”,会带来性能开销。

三. 分布式锁的应用

为了加深大家对Zookeeper的理解,接下来我就带着大家来模拟一个多进程环境下的卖票程序。

1.准备环境

我们先准备一个数据库,脚本如下,大家可以导入数据库创建自己的表格。

DROP TABLE IF EXISTS `ticket`;

CREATE TABLE `ticket` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `count` bigint(20) DEFAULT NULL,
  `identifier` varchar(32) DEFAULT NULL,
  `start` varchar(32) DEFAULT NULL,
  `end` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

/*Data for the table `ticket` */

insert  into `ticket`(`id`,`count`,`identifier`,`start`,`end`) values (1,0,'HZD-001','杭州东站','九江站'),(2,100,'HZD-002','杭州东站','南昌站');

2.创建Web项目

接下来我们利用SpringBoot来创建一个Web项目。

2.1 引入依赖

在这里引入一些关键的依赖包。

<dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>

      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
      </dependency>


      <dependency>
          <groupId>com.baomidou</groupId>
          <artifactId>mybatis-plus-boot-starter</artifactId>
          <version>3.5.0</version>
      </dependency>
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.26</version>
      </dependency>
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid-spring-boot-starter</artifactId>
          <version>1.2.8</version>
      </dependency>

      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>fastjson</artifactId>
          <version>1.2.8</version>
      </dependency>

      <dependency>
          <groupId>com.github.hiwepy</groupId>
          <artifactId>curator-spring-boot-starter</artifactId>
          <version>2.0.0.RELEASE</version>
      </dependency>

  </dependencies>

2.2 添加配置

在application.yml文件中进行必要的配置。

# 第一个进程配置
server:
  port: 81

spring:
  datasource:
    url: jdbc:mysql:///db3
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver

winnum: 1

curator:
  connect-string: localhost:2181
  session-timeout-ms: 5000

---
# 第二个进程配置
server:
  port: 82

winnum: 2

spring:
  profiles: win2

2.3 编写Ticket实体类

我们还要封装一个实体类,用于信息的封装。

@Data
public class Ticket {
    private Integer id;
    private Integer count;
    private String identifier;
    private String start;
    private String end;
}

2.4 编写Mapper接口

这是基于Mybatis-Plus实现的dao层操作接口。

public interface TicketMapper extends BaseMapper<Ticket> {
}

2.5 编写Controller访问接口

最后我们还要编写Controller层代码,实现一个简单的秒杀接口。

@RestController
@RequestMapping("/ticket")
public class TicketController {

    @Autowired
    private TicketMapper ticketMapper;

    @Value("${winnum}")
    private Integer winnum;

    @GetMapping("/sell/{id}")
    public void sell(@PathVariable("id") Integer id) throws Exception {
        while (true){
            Ticket ticket = ticketMapper.selectById(id);
            //查询是否有票
            if (ticket.getCount() > 0) {
                //有票
                System.out.println(winnum + "窗口正在卖出第" + ticket.getCount() + "张票");

                //模拟卖票耗时
                Thread.sleep(1000);

                ticket.setCount(ticket.getCount() - 1);

                //将新的票数设置到数据库
                ticketMapper.updateById(ticket);

                System.out.println(winnum + "窗口卖出票后,剩余票数为: " + ticket.getCount());
            } else {
                //没票
                break;
            }
        }
    }
}

至此,我们这个Web项目的核心代码就已经全部完成了,接下来我们需要把项目运行部署起来,测试一下抢购接口是否可用。

3.启动两个进程

因为我们要模拟多进程的环境,所以这里需要启动两个进程,这里的启动环境配置如下图所示:

最终两个进程启动效果如下:

4.分别访问两个进程开始卖票

访问进程1:http://localhost:81/ticket/sell/1

访问进程2:http://localhost:82/ticket/sell/1

通过观察上面的运行结果,大家可以看到这个卖票程序出现了问题!即两个程序一直在卖同一张票,这就出现了冲突,在现实场景下,这是不允许的!那么接下来,明明老师就带着大家,利用zookeeper的分布式锁将这个问题解决掉。

5.zookeeper分布式锁解决卖票问题

我们把上面的Controller接口进行改造,引入Zookeeper代码,重新编写一下售票程序。

@RestController
@RequestMapping("/ticket")
public class TicketController {

    @Autowired
    private TicketMapper ticketMapper;

    @Value("${winnum}")
    private Integer winnum;

    @Autowired
    private CuratorZkTemplate zkTemplate;

    public static final String LOCK_PREFIX = "/LOCK-";

    @GetMapping("/sell/{id}")
    public void sell(@PathVariable("id") Integer id) throws Exception {
        while (true){
            //通过分布式锁控制多进程间共享数据的操作安全
            InterProcessLock lock = zkTemplate.getSharedReentrantLock(LOCK_PREFIX+id);

            try {
                //加锁
                lock.acquire();

                Ticket ticket = ticketMapper.selectById(id);
                //查询是否有票
                if (ticket.getCount() > 0) {
                    //有票
                    System.out.println(winnum + "窗口正在卖出第" + ticket.getCount() + "张票");

                    //模拟卖票耗时
                    Thread.sleep(1000);

                    ticket.setCount(ticket.getCount() - 1);

                    //将新的票数设置到数据库
                    ticketMapper.updateById(ticket);

                    System.out.println(winnum + "窗口卖出票后,剩余票数为: " + ticket.getCount());
                } else {
                    //没票
                    break;
                }
            }finally {
                lock.release();
            }
        }
    }
}
在加锁时大家要注意:只有在卖相同列次的票时才需要进行互斥操作。对于多个进程,如果卖的是不同列次的票,是不需要互斥等待的,所以加锁的键一定要以票的id为标识。

代码修改后,我们再次来看看执行效果。

此时大家可以看到,上面的问题我们已经解决了,现在两个进程在卖票时,已经不会再出现同一张票被卖给多人的情况了。至此,问题解决,现在你学会Zookeeper的作用及其用法了吗?如果你还有其他疑问,可以在评论区留言哦。

扫码咨询了解课程详情,获取课程配套资料!



扫码或者点击文末左下角【阅读原文】快速进入学习!

点击【阅读原文】,免费开始学习Zookeeper视频课程!

原文作者:Java架构栈;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2021-04-13

  • 博文量
    573
  • 访问量
    243795