自己写分布式锁-基于redission

2,698 阅读10分钟

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

项目已经发布到GitHub,到目前有41个star,地址为github.com/TaXueWWL/re…

项目简介

该分布式锁名称为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

坐标为:

当前最新版为1.2.0 截止到2019.4.19

    <!--分布式锁redisson版本-->
    <dependency>
        <groupId>com.snowalker</groupId>
        <artifactId>redis-distributed-lock-starter</artifactId>
        <version>1.2.0</version>
    </dependency>
  1. 在项目的application.properties中增加redis连接配置,这里以单机模式为例。目前支持单机、集群、哨兵、主从等全部的链接方式,总有适合你的一个。注意配置密码及需要使用的数据库,密码如果没有默认为空
        ########################################################################
        #
        #     redisson配置
        #
        #########################################################################
        redisson.lock.server.address=127.0.0.1:6379
        redisson.lock.server.password=
        redisson.lock.server.database=1
        redisson.lock.server.type=standalone
  1. 在启动类添加注解,@EnableRedissonLock 打开Redisson分布式锁自动装配,如:
        @EnableRedissonLock
        @EnableScheduling
        @SpringBootApplication
        public class RedisDistributedLockStarterDemoApplication {
            public static void main(String[] args) throws Exception {
                SpringApplication.run(RedisDistributedLockStarterDemoApplication.class, args);
            }
        }
  1. 有两种调用方式,一种是直接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 包,我在这里使用了策略模式进行各个连接方式的实现工作。