第12课:分布式锁

第12课:分布式锁

分布式锁是什么?

根据百度百科定义,分布式锁是控制分布式系统之间同步访问共享资源的一种方式。使用它的意义在于,当不同系统或同一系统的不同服务器共享相同资源时,能够让它们互斥访问这些资源,以保证资源状态的一致性。

分布式锁主要包括以下几个特点:

  1. 可重入:当一个进程或者线程获得锁后,该进程或线程在未释放该锁之前,还能再次获得该锁;
  2. 互斥性:同一时刻,只能有一个客户端获得锁;
  3. 可释放:不管获得锁的客户端进程或线程是否正常结束,必须保证锁能在有限时间内被释放;
  4. 高可用、高性能:获取锁、释放锁的过程需保证高可用、高性能,只有这样才能保证分布式锁对系统自身业务影响最小。

实现分布式锁的三种常见方式

基于数据库表实现分布式锁

系统开发过程中,我们可以利用数据库表实现分布式锁,来锁住某个方法或者资源。该种方式容易理解,且实现简单,下面主要为大家介绍下实现步骤。

1. 在数据库中执行如下 SQL 语句建立表 methodLock:

CREATE TABLE methodLock (
  id INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  method_name VARCHAR(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
  description VARCHAR(1024) NOT NULL DEFAULT '描述信息',
  update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (id),
  UNIQUE KEY uidx_method_name (method_name ) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

注意: 方法名称字段 method_name 必须添加唯一性 UNIQUE 限制,只有这样才能保证同一时刻只有一个进程或者线程向表中插入一条与方法名称相同的记录,即同一时刻只有一个进程或线程获得锁。

2. 执行如下 SQL 语句,成功后即可获得分布式锁:

insert into methodLock(method_name,description) values('method_name','desc');

注意:'method_name' 表示当前需要被锁的方法名称,'desc' 表示相关描述信息。

3. 执行完分布式锁保护的方法或者资源相关的业务处理时,执行如下 SQL 语句释放锁:

delete from methodLock where method_name = 'method_name';  

经过如上三步,我们基于数据库表实现了分布式锁。

从上面的实现过程,我们可以看出该方法实现简单,易于理解。另外,该方法也存在一定的问题,比如:

  1. 非重入锁;
  2. 数据库是单点,当数据库宕机后,无法获取分布式锁;
  3. 分布式锁无超时时间设置,当应用服务器宕机后,分布式锁无法释放,容易导致死锁。

基于 Redis 实现分布式锁

为了降低利用 Redis 实现分布式锁的门槛,Redis 官方推荐开发者使用 Redisson 实现分布式锁。下面为大家演示如何利用 Spring Boot 整合 Redisson 实现分布式锁。

1. 新建 Maven 项目 distributedLock,并在其 pom 文件中添加如下依赖:

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

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>

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

注意: 这里务必要注意 Redisson 的版本问题。刚开始我采用的是 3.4.2 版本,程序启动时就报错 “Caused by: java.lang.IllegalArgumentException: port out of range:-1”,之后换成 3.6.5 版本,便能正常启动了。

2. 新建 application.properties 文件,并在文件中添加如下配置内容:

server.port=8080

redisson.address=redis://192.168.1.120:6379
redisson.password=hcb13579
redisson.timeout=3000

注意:redisson.address 需要以 redis:// 为前缀,否则无法连接 Redis。

3. 新建分布式锁接口及其实现类。

分布式锁接口定义如下:

public interface DistributedLocker {
    /**
     * 加锁
     * @param lockKey
     */
    void lock(String lockKey);

    /**
     * 释放锁
     * @param lockKey
     */
    void unLock(String lockKey);


    /**
     * 带有超时时间加锁
     * @param lockKey
     * @param timeout
     */
    void lock(String lockKey,int timeout);

    /**
     * 带有超时时间加锁,并指定时间单位
     * @param lockKey
     * @param timeout
     * @param timeUnit
     */
    void lock(String lockKey, int timeout, TimeUnit timeUnit);
}

利用 RedissonClient 实现分布式锁接口,代码如下:

public class RedissonDistributedLocker implements DistributedLocker {
    private RedissonClient redissonClient;


    public void setRedissonClient(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    ...此处省略部分实现代码
 }

4. 定义获取 Redis 相关信息配置类,代码如下:

@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {
    //redis连接地址
    private String address;
    //访问redis密码
    private String password;

    ...此处省略部分实现代码
}

5. 新建 RedissonClient、RedissonDistributedLocker 实例,并初始化 RedissonLockUtil 工具类中 DistributedLocker 对象:

@Configuration
@ConditionalOnClass(Config.class)
@EnableConfigurationProperties(RedissonProperties.class)
public class RedissonAutoConfiguration {
    @Autowired
    private RedissonProperties redssonProperties;

    @Bean
    @ConditionalOnProperty(name="redisson.address")
    RedissonClient redissonSingle() {
        Config config = new Config();
        //设置serverConfig连接地址信息、超时时间等信息
        SingleServerConfig serverConfig = config.useSingleServer()
               .setAddress(redssonProperties.getAddress())
             .setTimeout(redssonProperties.getTimeout())
                .setConnectionPoolSize(redssonProperties.getConnectionPoolSize())
                .setConnectionMinimumIdleSize(redssonProperties.getConnectionMiniumIdleSize());

        if(!StringUtils.isEmpty(redssonProperties.getPassword())) {
            serverConfig.setPassword(redssonProperties.getPassword());
        }

        //返回RedissonClient实例
        return Redisson.create(config);
    }

    @Bean
    DistributedLocker distributedLocker(RedissonClient redissonSingle) {
        RedissonDistributedLocker locker = new RedissonDistributedLocker();
        //设置RedissonDistributedLocker对象中RedissonClient对象
        locker.setRedissonClient(redissonSingle());
        //设置工具类RedissonLockUtil中DistributedLocker对象
        RedissonLockUtil.setLocker(locker);
        return locker;
    }
}

6. 在控制器 DistributedLockController 中新增接口 distributedLock,并利用多线程模拟分布式锁功能。此步骤的代码就不贴了,大家可在文末提供的 Github 地址中查看、下载。

7. 依次启动 Redis、应用程序,并利用 Postman 调用接口:http://localhost:8080/lock/redissonLock,对分布式锁功能进行测试,截图如下:

enter image description here

接下来,我们看下这种方法的优点及存在的问题。

优点主要有:

  1. Redis 基于内存操作,性能比较好;
  2. 可以设置超时时间,当客户端与 Redis Server 断开后,锁仍然能被释放。

主要存在的问题包括:

  1. 非重入锁;
  2. 靠开发者的经验来预估锁超时时间,很难准确。

基于 ZooKeeper 实现分布式锁

ZooKeeper 基于其临时有序节点实现分布式锁,大致思路如下:

  1. 客户端与 ZooKeeper 建立连接;
  2. 在指定节点目录下新建一个唯一、有序临时节点;
  3. 判断当前生成节点是否为指定节点目录下的最小序号节点,如果是,则获取分布式锁成功,否则监听序号比其大的第一个节点,当序号最小节点删除时,当前节点再次发起获取分布式锁;
  4. 释放锁时,删除临时节点;

为了简化分布式锁的实现,下面利用 Curator 客户端为大家演示如何利用 ZooKeeper 实现分布式锁。

1. 在 distributedLock 项目的 Pom 文件中添加如下依赖:

<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.8.0</version>
</dependency>

<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.8.0</version>
</dependency>

注意: 添加 curator-frameworkcurator-recipes 时,务必注意它们的版本问题,如果版本不一致,启动时程序将报错。

2. 新建控制器 CuratorLockController,并在该类中添加 curatorLock 方法,该类的定义如下:

@RestController
@RequestMapping("/curator")
public class CuratorLockController {
    //zookeeper集群连接字符串
    private static final String connectionUrl = "192.168.1.120:2181,192.168.1.121:2181," +
            "192.168.1.6:2181";

    //zookeeper连接最大超时时间
    private static final Integer connectionTimeout = 5000;

    private static CuratorFramework cf;

    static {
        //重试策略:初试时间为1s,重试10次
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 10);
        //创建连接
        cf = CuratorFrameworkFactory.builder()
                .connectString(connectionUrl)
                .sessionTimeoutMs(connectionTimeout)
                .retryPolicy(retryPolicy)
                .build();
        //开启连接
        cf.start();
    }

    @RequestMapping("/lock")
    public String curatorLock() throws Exception {
         ...此处省略部分代码
         if (lock.acquire(5000, TimeUnit.SECONDS)) {
             ...执行业务逻辑                    
         }
    }
}

注意:lock.acquire(5000, TimeUnit.SECONDS) 方法调用中,5000 表示最大获取锁时间,该时间应该根据线程业务逻辑处理时间的长短来设置。设置太小,将导致有些线程获取锁失败,最后导致线程业务逻辑无法被处理。

3. 按顺序分别启动 ZooKeeper 集群、应用程序,利用 Postman 调用接口:http://localhost:8080/curator/lock,输出截图如下:

enter image description here

接下来,我们看下该方法的优点及存在的问题。

优点主要有:

  1. 该方法易实现高可用;
  2. 通过建立 ZooKeeper 临时、顺序节点易实现分布式锁的释放,避免发生死锁。

主要存在的问题包括:

  1. 可能存在锁并发使用问题;
  2. 性能不是太好。

结语

本文主要介绍了三种分布式锁实现方式,整体了解下来没有一种实现方式是完美无缺点的。大家在使用过程中可以根据自身业务要求及应用场景进行选择,毕竟技术最终要服务于业务。

课程源码下载地址:

GitHub

上一篇
下一篇
目录