文章目录
  1. 1. 项目简介
  2. 2. 如何使用?
  3. 3. 代码及实现原理分析
    1. 3.1. 1.redis连接配置,RedissonManager
    2. 3.2. 2. redis连接类型枚举RedisConnectionType
    3. 3.3. 3. 分布式锁核心实现RedissonLock
  4. 4. 注解支持
    1. 4.1. 1. 注解定义
    2. 4.2. 2. 注解解析类DistributedLockHandler
    3. 4.3. 添加自动装配支持
  5. 5. 附录:redis配置方式
    1. 5.1. redisson分布式锁配置–单机
    2. 5.2. redisson分布式锁配置–哨兵
    3. 5.3. redisson分布式锁配置–集群方式
    4. 5.4. redisson分布式锁配置–主从

之前的文章中,我们利用Redis实现了分布式限流组件,文章链接:自己写分布式限流组件-基于Redis的RateLimter ,不得不感叹Redis功能的强大,本文中我们继续利用Redis的特性,基于Redission组件,实现一款能注解支持的可靠分布式锁组件。

项目已经发布到GitHub,到目前有41个star,地址为https://github.com/TaXueWWL/redis-distributed-lock

项目简介

该分布式锁名称为redis-distributed-lock,是一个多module的工程。提供纯Java方式调用,支持传统Spring工程, 为spring boot应用提供了starter,开箱即用。

项目的目录结构及其描述如下:

项目 描述
redis-distributed-lock-core 原生redis分布式锁实现,支持注解,不推荐项目中使用,仅供学习使用
redis-distributed-lock-demo-spring redis-distributed-lock-core 调用实例,仅供学习
redis-distributed-lock-starter 基于Redisson的分布式锁spring starter实现,可用于实际项目中
redis-distributed-lock-starter-demo redis-distributed-lock-starter调用实例

由于篇幅限制, redis-distributed-lock-core 及 redis-distributed-lock-demo-spring这两个工程我就不在本文中间介绍了,感兴趣的同学可以看我这篇文章的redis分布式锁部分,就是介绍的这两个工程的原理实现。 分布式锁的多种实现

本文主要讲解一下redis-distributed-lock-starter的使用及实现机制,首先说一下如何使用吧,这样能够直观的对它进行一个较为全面的了解,后面讲到代码实现能够更好的理解其机制。

如何使用?

redis-distributed-lock-starter是一个spring-boot-starter类的类库,关于starter的实现机制,可以看我另一篇文章 Springboot自动配置魔法之自定义starter

  1. 首先保证当前工程为一个springboot项目,然后添加starter的坐标依赖。(需要下载源码在本地构建,并执行mvn clean install -DskipTests

坐标为:

<!--分布式锁redisson版本-->
<dependency>
    <groupId>com.snowalker</groupId>
    <artifactId>redis-distributed-lock-starter</artifactId>
    <version>1.0.0</version>
</dependency>
  1. 在项目的application.properties中增加redis连接配置,这里以单机模式为例。目前支持单机、集群、哨兵、主从等全部的链接方式,总有适合你的一个。

    redisson.lock.server.address=127.0.0.1:6379
    redisson.lock.server.type=standalone
    
  2. 有两种调用方式,一种是直接java代码编程方式调用,一种是注解支持。

    1. 直接编程方式调用如下,在需要加锁的定时任务中,注入 RedissonLock 实体,即可进行加锁、解锁等操作。 锁自动释放时间默认为10秒,这个时间需要你根据自己的业务执行时间自行指定。

      @Autowired
      RedissonLock redissonLock;    
      @Scheduled(cron = "${redis.lock.cron}")
      public void execute() throws InterruptedException {
          if (redissonLock.lock("redisson", 10)) {
              LOGGER.info("[ExecutorRedisson]--执行定时任务开始,休眠三秒");
              Thread.sleep(3000);
              System.out.println("=======业务逻辑===============");
              LOGGER.info("[ExecutorRedisson]--执行定时任务结束,休眠三秒");
              redissonLock.release("redisson");
          } else {
              LOGGER.info("[ExecutorRedisson]获取锁失败");
          }
      }
      
    2. 注解方式调用如下,在需要加锁的定时任务的执行方法头部,添加 @DistributedLock(value = “redis-lock”, expireSeconds = 11) 即可进行加锁、解锁等操作(value表示锁在redis中存放的key值,expireSeconds表示加锁时间)。锁自动释放时间默认为10秒,这个时间需要你根据自己的业务执行时间自行指定。我这里以spring schedule定时任务为例,用其他的定时任务同理,只需要添加注解。

      @Scheduled(cron = "${redis.lock.cron}")
      @DistributedLock(value = "redis-lock", expireSeconds = 11)
      public void execute() throws InterruptedException {
          LOGGER.info("[ExecutorRedisson]--执行定时任务开始,休眠三秒");
          Thread.sleep(3000);
          System.out.println("======业务逻辑=======");
          LOGGER.info("[ExecutorRedisson]--执行定时任务结束,休眠三秒");
      }
      
    3. 你可以改变测试demo的端口,起多个查看日志,能够看到同一时刻只有一个实例获取锁成功并执行业务逻辑

调用日志如下所示,可以看出,多个进程同一时刻只有一个运行,表明我们的锁添加成功且生效。

2018-07-11 09:48:06.330 |-INFO  [main] com.snowalker.RedisDistributedLockStarterDemoApplication [57] -| 
Started RedisDistributedLockStarterDemoApplication in 3.901 seconds (JVM running for 4.356)
2018-07-11 09:48:10.006 |-INFO  [pool-3-thread-1] com.snowalker.lock.redisson.annotation.DistributedLockHandler [32] -| 
[开始]执行RedisLock环绕通知,获取Redis分布式锁开始
2018-07-11 09:48:10.622 |-INFO  [pool-3-thread-1] com.snowalker.lock.redisson.RedissonLock [35] -| 
获取Redisson分布式锁[成功],lockName=redis-lock
2018-07-11 09:48:10.622 |-INFO  [pool-3-thread-1] com.snowalker.lock.redisson.annotation.DistributedLockHandler [39] -| 
获取Redis分布式锁[成功],加锁完成,开始执行业务逻辑...
2018-07-11 09:48:10.625 |-INFO  [pool-3-thread-1] com.snowalker.executor.ExecutorRedissonAnnotation [22] -|
 [ExecutorRedisson]--执行定时任务开始,休眠三秒
=======================业务逻辑=============================
2018-07-11 09:48:13.625 |-INFO  [pool-3-thread-1] com.snowalker.executor.ExecutorRedissonAnnotation [25] -| 
[ExecutorRedisson]--执行定时任务结束,休眠三秒
2018-07-11 09:48:13.627 |-INFO  [pool-3-thread-1] com.snowalker.lock.redisson.annotation.DistributedLockHandler [46] -| 
释放Redis分布式锁[成功],解锁完成,结束业务逻辑...
2018-07-11 09:48:13.628 |-INFO  [pool-3-thread-1] com.snowalker.lock.redisson.annotation.DistributedLockHandler [50] -| 
[结束]执行RedisLock环绕通知

使用还是比较简单的,接下来我们走进代码细节,看一下如何实现一个易用的分布式锁组件。

代码及实现原理分析

为了符合开放封闭原则,所以我们只要把编码方式的分布式锁实现设计好,那么将其扩张成注解形式的就很容易。

1.redis连接配置,RedissonManager

由于我们使用的是Redission对Redis操作,因此首先建立一个RedissonManager类,用于提供初始化的redisson实例给核心业务使用。

代码如下

public class RedissonManager {

    private static final Logger LOGGER = LoggerFactory.getLogger(Redisson.class);

    private Config config = new Config();

    private Redisson redisson = null;

    public RedissonManager() {}

    public RedissonManager (String connectionType, String address) {
        try {
            config = RedissonConfigFactory.getInstance().createConfig(connectionType, address);
            redisson = (Redisson) Redisson.create(config);
        } catch (Exception e) {
            LOGGER.error("Redisson init error", e);
            e.printStackTrace();
        }
    }

    public Redisson getRedisson() {
        return redisson;
    }

    /**
    * Redisson连接方式配置工厂
    */
    static class RedissonConfigFactory {
        private RedissonConfigFactory() {}
        private static volatile RedissonConfigFactory factory = null;

        public static RedissonConfigFactory getInstance() {
            if (factory == null) {
                synchronized (RedissonConfigFactory.class) {
                    factory = new RedissonConfigFactory();
                }
            }
            return factory;
        }

        private Config config = new Config();
        /**
        * 根据连接类型及连接地址参数获取对应连接方式的配置,基于策略模式
        * @param connectionType
        * @param address
        * @return Config
        */
        Config createConfig(String connectionType, String address) {
            Preconditions.checkNotNull(connectionType);
            Preconditions.checkNotNull(address);
            /**声明配置上下文*/
            RedissonConfigContext redissonConfigContext = null;
            if (connectionType.equals(RedisConnectionType.STANDALONE.getConnection_type())) {
                redissonConfigContext = new RedissonConfigContext(new StandaloneRedissonConfigStrategyImpl());
            } else if (connectionType.equals(RedisConnectionType.SENTINEL.getConnection_type())) {
                redissonConfigContext = new RedissonConfigContext(new SentinelRedissonConfigStrategyImpl());
            } else if (connectionType.equals(RedisConnectionType.CLUSTER.getConnection_type())) {
                redissonConfigContext = new RedissonConfigContext(new ClusterRedissonConfigStrategyImpl());
            } else if (connectionType.equals(RedisConnectionType.MASTERSLAVE.getConnection_type())) {
                redissonConfigContext = new RedissonConfigContext(new MasterslaveRedissonConfigStrategyImpl());
            } else {
                throw new RuntimeException("创建Redisson连接Config失败!当前连接方式:" + connectionType);
            }
            return redissonConfigContext.createRedissonConfig(address);
        }
    }
}

很好理解,通过构造方法,我们将Redis连接类型(包括:单机STANDALONE、集群CLUSTER、主从MASTERSLAVE、哨兵SENTINEL)以及rredis地址注入,并调用内部类RedissonConfigFactory工厂,生产出对应的Redis连接配置。

这里我使用了策略模式,根据构造方法传递的连接类型选择不同的连接实现,从配置上下文RedissonConfigContext中取出对应的模式的连接。这里不是我们的重点,感兴趣的同学们可以自行查看代码实现。

2. redis连接类型枚举RedisConnectionType

public enum RedisConnectionType {

    STANDALONE("standalone", "单节点部署方式"),
    SENTINEL("sentinel", "哨兵部署方式"),
    CLUSTER("cluster", "集群方式"),
    MASTERSLAVE("masterslave", "主从部署方式");

    private final String connection_type;
    private final String connection_desc;

    private RedisConnectionType(String connection_type, String connection_desc) {
        this.connection_type = connection_type;
        this.connection_desc = connection_desc;
    }

    public String getConnection_type() {
        return connection_type;
    }

    public String getConnection_desc() {
        return connection_desc;
    }
}

该枚举为穷举出的目前支持的四种Redis连接方式。

3. 分布式锁核心实现RedissonLock

RedissonLock是本工程的核心实现类,我们边看代码边解释

public class RedissonLock {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedissonLock.class);

    RedissonManager redissonManager;

    public RedissonLock(RedissonManager redissonManager) {
        this.redissonManager = redissonManager;
    }

这里通过构造方法将之前定义的RedissonManager注入锁实例中,用于在建立好的连接上获取RLock进行进一步的操作。

RLock是Redisson的分布式锁实现,原理也是基于setnx,只不过Redisson包装的更加优雅易用。

Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁。感兴趣的可以自行找资料学习,本文不展开讲解了。

public RedissonLock() {}
/**
 * 加锁操作
 * @return
 */
public boolean lock(String lockName, long expireSeconds) {
    RLock rLock = redissonManager.getRedisson().getLock(lockName);
    boolean getLock = false;
    try {
        getLock = rLock.tryLock(0, expireSeconds, TimeUnit.SECONDS);
        if (getLock) {
            LOGGER.info("获取Redisson分布式锁[成功],lockName={}", lockName);
        } else {
            LOGGER.info("获取Redisson分布式锁[失败],lockName={}", lockName);
        }
    } catch (InterruptedException e) {
        LOGGER.error("获取Redisson分布式锁[异常],lockName=" + lockName, e);
        e.printStackTrace();
        return false;
    }
    return getLock;
}

lock(String lockName, long expireSeconds) 方法是核心加锁实现,我们设置了锁的名称,用于对应用进行区分,从而支持多应用的多分布式锁实现。

进入方法中,从连接中获取到RLock实现,调用boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) 设置传入的锁过期时间,并立即尝试获取锁。如果返回true则表明加锁成功,否则为加锁失败。

注意:加锁的时间要大于业务执行时间,这个时间需要通过测试算出最合适的值,否则会造成加锁失败或者业务执行效率过慢等问题。

/**
 * 解锁
 * @param lockName
 */
public void release(String lockName) {
    redissonManager.getRedisson().getLock(lockName).unlock();
}

这个方法就比较好理解,在需要解锁的位置调用该方法,对存在的锁做解锁操作,内部实现为对setnx的值做过期处理。

注解支持

有了基本的java编程式实现,我们就可以进一步实现注解支持。

1. 注解定义

首先定义注解,支持方法级、类级限流。

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DistributedLock {

    /**分布式锁名称*/
    String value() default "distributed-lock-redisson";
    /**锁超时时间,默认十秒*/
    int expireSeconds() default 10;
}

定义两个属性,value表示标注当前锁的key,建议命名规则为:应用名:模块名:方法名:版本号,从而更细粒度的区分。expireSeconds表示锁超时时间,默认10秒,超过该时间锁自动释放,可以用于下一次争抢。

2. 注解解析类DistributedLockHandler

接着我们定义一个注解解析类,这里使用aspectj实现。

@Aspect
@Component
public class DistributedLockHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(DistributedLockHandler.class);

    @Pointcut("@annotation(com.snowalker.lock.redisson.annotation.DistributedLock)")
    public void distributedLock() {}

    @Autowired
    RedissonLock redissonLock;

    @Around("@annotation(distributedLock)")
    public void around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
        LOGGER.info("[开始]执行RedisLock环绕通知,获取Redis分布式锁开始");
        /**获取锁名称*/
        String lockName = distributedLock.value();
        /**获取超时时间,默认十秒*/
        int expireSeconds = distributedLock.expireSeconds();
        if (redissonLock.lock(lockName, expireSeconds)) {
            try {
                LOGGER.info("获取Redis分布式锁[成功],加锁完成,开始执行业务逻辑...");
                joinPoint.proceed();
            } catch (Throwable throwable) {
                LOGGER.error("获取Redis分布式锁[异常],加锁失败", throwable);
                throwable.printStackTrace();
            }
            redissonLock.release(lockName);
            LOGGER.info("释放Redis分布式锁[成功],解锁完成,结束业务逻辑...");
        } else {
            LOGGER.error("获取Redis分布式锁[失败]");
        }
        LOGGER.info("[结束]执行RedisLock环绕通知");
    }
}

我们使用环绕切面在业务逻辑之前进行加锁操作,如果加锁成功则执行业务逻辑,执行结束后,进行锁释放工作。这里需要优化一下,就是将解锁放到finally中。保证业务逻辑执行完成必定会释放锁。

到这里,我们就基本完成springboot支持的分布式锁实现,还差一点步骤。

添加自动装配支持

我们在resources下建立一个目录,名为META-INF , 并在其中定义一个文件,名为spring.factories,并在其中添加如下内容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.snowalker.lock.redisson.config.RedissonAutoConfiguration

这样做之后,当依赖该starter的项目启动之后,会自动装配我们的分布式锁相关的实体,从而实现自动化的配置。等应用启动完成之后,就会自动获取锁配置。

附录:redis配置方式

前文已经提到,我们的分布式支持各种形式的redis连接方式,下面展开说明一下,实际使用的时候可以参考这里的配置,结合实际的redis运行模式进行配置。

redisson分布式锁配置–单机

redisson.lock.server.address=127.0.0.1:6379
redisson.lock.server.type=standalone

redisson分布式锁配置–哨兵

redisson.lock.server.address 格式为: sentinel.conf配置里的sentinel别名,sentinel1节点的服务IP和端口,sentinel2节点的服务IP和端口,sentinel3节点的服务IP和端口

比如sentinel.conf里配置为sentinel monitor my-sentinel-name 127.0.0.1 6379 2,那么这里就配置my-sentinel-name

redisson.lock.server.address=my-sentinel-name,127.0.0.1:26379,127.0.0.1:26389,127.0.0.1:26399
redisson.lock.server.type=sentinel

redisson分布式锁配置–集群方式

cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)

地址格式为: 127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384

redisson.lock.server.address=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384
redisson.lock.server.type=cluster

redisson分布式锁配置–主从

地址格式为主节点,子节点,子节点

比如:127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381

代表主节点:127.0.0.1:6379,从节点127.0.0.1:6380,127.0.0.1:6381

redisson.lock.server.address=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381
redisson.lock.server.type=masterslave

具体的实现过程,请参考源码的com.snowalker.lock.redisson.config.strategy 包,我在这里使用了策略模式进行各个连接方式的实现工作。

文章目录
  1. 1. 项目简介
  2. 2. 如何使用?
  3. 3. 代码及实现原理分析
    1. 3.1. 1.redis连接配置,RedissonManager
    2. 3.2. 2. redis连接类型枚举RedisConnectionType
    3. 3.3. 3. 分布式锁核心实现RedissonLock
  4. 4. 注解支持
    1. 4.1. 1. 注解定义
    2. 4.2. 2. 注解解析类DistributedLockHandler
    3. 4.3. 添加自动装配支持
  5. 5. 附录:redis配置方式
    1. 5.1. redisson分布式锁配置–单机
    2. 5.2. redisson分布式锁配置–哨兵
    3. 5.3. redisson分布式锁配置–集群方式
    4. 5.4. redisson分布式锁配置–主从
Fork me on GitHub