springboot-redis-redisson分布式锁

springboot-redis-redisson分布式锁

  • 一、Redis分布式锁实现原理
    • 简介
    • 1.普通分布式锁
    • 2.哨兵模式
    • 3.集群模式
    • 唯一ID
    • 看门狗Watchdog
    • 可重入锁
    • 获取锁
    • 释放锁
  • 二、 完整代码
    • 依赖
    • application.properties配置
    • 配置类
    • 锁后业务接口规范
    • redis加锁接口规范
    • redis加锁实现类
    • 自定义异常
    • 测试类

比较:redis很明显优于zookeeper;就分布式锁实现的健壮性而言,zookeeper很明显优于redis。如何选择,取决于你的业务!

一、Redis分布式锁实现原理

springboot-redis-redisson分布式锁_第1张图片

简介

Redis的4种部署架构有:

  • 单机模式;
  • 主从模式;
  • 哨兵模式;
  • 集群模式;

Redlock分布式锁在非单机模式下才有意义,单机模式下可以直接使用普通分布式锁。

因为RedLock的实现完全基于普通分布式锁;

RedLock加锁过程: 向所有节点发送 set key value ex time nx指令,只要过半节点set成功,就认为加锁成功。
RedLock释放锁过程: 向所有节点发送del指令;

因为RedLock操作多个节点,所以效率略有下降;但避免了网络分区产生的多锁同时存在的情况;

1.普通分布式锁

// 构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("1.9.1.0:5379").setPassword("123").setDatabase(0);
// 构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 设置锁定资源名称
RLock disLock = redissonClient.getLock("DISLOCK");
boolean isLock;
try {
     
    //尝试获取分布式锁
    isLock = disLock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
    if (isLock) {
     
        //TODO if get lock success, do something;
        Thread.sleep(15000);
    }
} catch (Exception e) {
     
} finally {
     
    // 无论如何, 最后都要解锁
    disLock.unlock();
}

通过代码可知,经过Redisson的封装,实现Redis分布式锁非常方便,我们再看一下Redis中的value是啥,和前文分析一样,hash结构,key就是资源名称,field就是UUID+threadId,value就是重入值,在分布式锁时,这个值为1(Redisson还可以实现重入锁,那么这个值就取决于重入次数了):

172.29.1.180:5379> hgetall DISLOCK
1) "01a6d806-d282-4715-9bec-f51b9aa98110:1"
2) "1"

2.哨兵模式

即sentinel模式,实现代码和单机模式几乎一样,唯一的不同就是Config的构造:

Config config = new Config();
config.useSentinelServers().addSentinelAddress(
        "redis://127.0.0.1:26378","redis://127.0.0.1:26379", "redis://127.0.0.1:26380")
        .setMasterName("mymaster")
        .setPassword("a123456").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
// 还可以getFairLock(), getReadWriteLock()
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
     
    isLock = redLock.tryLock();
    // 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    if (isLock) {
     
        //TODO if get lock success, do something;
    }
} catch (Exception e) {
     
} finally {
     
    // 无论如何, 最后都要解锁
    redLock.unlock();
}

3.集群模式

集群模式构造Config如下:

Config config = new Config();
config.useClusterServers().addNodeAddress(
        "redis://127.0.0.1:6375","redis://127.0.0.1:6376", "redis://127.0.0.1:6377",
        "redis://127.0.0.1:6378","redis://127.0.0.1:6379", "redis://127.0.0.1:6380")
        .setPassword("123").setScanInterval(5000);
RedissonClient redissonClient = Redisson.create(config);
// 还可以getFairLock(), getReadWriteLock()
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
     
    isLock = redLock.tryLock();
    // 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    if (isLock) {
     
        //TODO if get lock success, do something;
    }
} catch (Exception e) {
     
} finally {
     
    // 无论如何, 最后都要解锁
    redLock.unlock();
}

如果是多个没有关系的redis单机之间使用RedLock来做分布式锁
可以直接将各个单机的锁组成一个锁:

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

唯一ID

实现分布式锁的一个非常重要的点就是set的value要具有唯一性,redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId。入口在redissonClient.getLock(“REDLOCK_KEY”),源码在Redisson.java和RedissonLock.java中:

protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
     
    return id + ":" + threadId;
}

看门狗Watchdog

springboot-redis-redisson分布式锁_第2张图片
看门狗是客户端的行为,创建锁后每隔10秒帮你把key的超时时间设为30s这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。
lockWatchdogTimeout(监控锁的看门狗超时,单位:毫秒)
默认值:30000

监控锁的看门狗超时时间单位为毫秒。该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。如果该看门狗未使用lockWatchdogTimeout去重新调整一个分布式锁的lockWatchdogTimeout超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况。

可重入锁

key是唯一的,key里面的field为uuid:线程Id。uuid由调用系统生成,一个系统中同一个锁只会用同一个uuid,不通系统的相同锁的uuid不一样。
所以根据uuid+线程id就可以确定当前线程是否拥有锁,如果拥有,就把value+1,释放锁就-1;

获取锁

获取锁的代码为redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS),两者的最终核心源码都是下面这段代码,只不过前者获取锁的默认租约时间(leaseTime)是LOCK_EXPIRATION_INTERVAL_SECONDS,即30s:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
     
    internalLockLeaseTime = unit.toMillis(leaseTime);
    // 获取锁时向5个redis实例发送的命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              // 首先分布式锁的KEY不能存在,如果确实不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间)
              "if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              // 如果分布式锁的KEY已经存在,并且value也匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间
              "if (redis.call('hexists', KEYS[1], [2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              // 获取分布式锁的KEY的失效时间毫秒数
              "return redis.call('pttl', KEYS[1]);",
              // 这三个参数分别对应KEYS[1],ARGV[1]和ARGV[2]
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

获取锁的命令中,

KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;

ARGV[1]就是internalLockLeaseTime,即锁的租约时间,默认30s;

ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值,即UUID+threadId:

释放锁

释放锁的代码为redLock.unlock(),核心源码如下:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
     
    // 向5个redis实例都执行如下命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 如果分布式锁KEY不存在,那么向channel发布一条消息
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            // 如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            // 如果就是当前线程占有分布式锁,那么将重入次数减1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            // 重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
                // 重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            // 这5个参数分别对应KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}

二、 完整代码

依赖


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.4.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.examplegroupId>
    <artifactId>springboot-redisartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>springboot-redisname>
    <description>Demo project for Spring Bootdescription>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.3.2version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>io.projectreactorgroupId>
            <artifactId>reactor-testartifactId>
            <scope>testscope>
        dependency>

    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

application.properties配置

# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.1.7
#spring.redis.cluster.nodes=127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
#spring.redis.password=123
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8

# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=200
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000

配置类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * Redis配置文件的参数
 */
@Data
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
     
    private String host;
    private int port;
    private String password;
    private int database;
}

Redission配置类,主要作用就是建立redis连接,生成client对象


import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Redission配置类,主要作用就是建立redis连接,生成client对象
 */
@Slf4j
@Configuration
public class RedissionConfig {
     

    @Autowired
    private RedisProperties redisProperties;

    @Bean
    public RedissonClient redissonClient() {
     
        RedissonClient redissonClient;

        Config config = new Config();
        String url =  redisProperties.getHost() + ":" + redisProperties.getPort();
        config.useSingleServer().setAddress(url)
                .setPassword(redisProperties.getPassword())
                .setDatabase(redisProperties.getDatabase());

        try {
     
            redissonClient = Redisson.create(config);
            return redissonClient;
        } catch (Exception e) {
     
            log.error("RedissonClient init redis url:[{}], Exception:", url, e);
            return null;
        }
    }

}

锁后业务接口规范

规定了获得锁后,如何调用业务逻辑

/**
 * 锁后业务接口规范
 */
public interface AquiredLockWorker<T> {
     
     T invokeAfterLockAquire() throws Exception;
}

redis加锁接口规范

规定了如何调用加锁规范

/**
 * redis加锁接口规范
 */
public interface RedisLocker {
     

     /**
      * 获取锁
      * @param lockName  锁的名称
      * @param worker 获取锁后的处理类
      * @param 
      * @return 处理完具体的业务逻辑要返回的数据
      * @throws UnableToAquireLockException
      * @throws Exception
      */
     <T> T lock(String lockName, AquiredLockWorker<T> worker) throws UnableToAquireLockException, Exception;

     <T> T lock(String lockName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception;
}

redis加锁实现类

实现了如何加锁、释放锁;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * redis加锁实现类
 */
@Component
public class RedisLockerImpl implements RedisLocker {
     

    private final static String LOCKER_PREFIX = "lock:";

    @Resource(name = "redissonClient")
    private RedissonClient redissonClient;

    @Override
    public <T> T lock(String lockName, AquiredLockWorker<T> worker) throws InterruptedException, UnableToAquireLockException, Exception {
     

        return lock(lockName, worker, 100);
    }

    @Override
    public <T> T lock(String lockName, AquiredLockWorker<T> worker, int lockTime) throws UnableToAquireLockException, Exception {
     
        //加锁
        RLock lock = redissonClient.getLock(LOCKER_PREFIX + lockName);
        // Wait for 100 seconds seconds and automatically unlock it after lockTime seconds
        //加锁等待100秒,100秒后仍未获得锁则返回false。获得锁后lockTime后自动释放锁。
        boolean success = lock.tryLock(100, lockTime, TimeUnit.SECONDS);
        if (success) {
     
            try {
     
                return worker.invokeAfterLockAquire();
            } finally {
     
                lock.unlock();
            }
        }
        throw new UnableToAquireLockException();
    }
}

自定义异常

/**
 * 异常类
 */
public class UnableToAquireLockException extends RuntimeException {
     

    public UnableToAquireLockException() {
     
    }

    public UnableToAquireLockException(String message) {
     
        super(message);
    }

    public UnableToAquireLockException(String message, Throwable cause) {
     
        super(message, cause);
    }
}

测试类

生成多个线程,进行锁竞争,完成业务逻辑。

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

/**
 * @Author ccl
 * @Date 2021/1/17 14:18
 */
@Slf4j
@RestController
public class LockTestController {
     


    @Autowired
    private RedisLocker redisLocker;

    @RequestMapping(value = "/redlock")
    public String testRedlock() throws Exception {
     

        CountDownLatch startSignal = new CountDownLatch(1);
        CountDownLatch doneSignal = new CountDownLatch(5);
        for (int i = 0; i < 5; ++i) {
     
            // create and start threads
            new Thread(() -> {
     
                try {
     
                    startSignal.await();
                    //主要代码在这里,第一个参数为锁的名字,第二个参数为加锁后要执行的业务
                    redisLocker.lock("test", () -> {
     
                        doTask();
                        doneSignal.countDown();
                        return null;
                    });
                } catch (Exception e) {
     
                    e.printStackTrace();
                }
            }).start();
        }
        startSignal.countDown(); // let all threads proceed
        doneSignal.await();
        log.info("All processors done. Shutdown connection");
        return "redlock";
    }

    /**
     * 加锁后要执行的业务逻辑
     */
    void doTask() {
     
        log.info(Thread.currentThread().getName() + " start");
        Random random = new Random();
        int _int = random.nextInt(200);
        log.info(Thread.currentThread().getName() + " sleep{}millis", _int);
        try {
     
            Thread.sleep(_int);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        log.info("{}end", Thread.currentThread().getName());

    }
}

你可能感兴趣的:(springboot,java,redis,分布式)