Redis实现分布式锁的正确方式

前言

上一篇文章讲的是  redis + lua实现 分布式限流,这篇文章是在上篇文章的项目结构添加了 分布锁的相关代码,如果碰到说个别的pom或者配置没有贴出来,请查看我的上篇文章 :https://blog.csdn.net/weixin_38003389/article/details/89049135

本文介绍的是利用 redis 实现分布式锁,redis单机操作。可能很多人看到这篇文章之前也会看其他兄台写的。分布式锁无非就两个操作,第一步“上锁”,第二步“解锁”,网上案例在上锁的操作上会有很大区别,本文在对上锁的操作采用一步到位,保证上锁操作的原子性,我觉得总比两步操作的姿势要舒服的多吧。

正文

 

介绍一下本次使用所有框架和中间件的版本

 

环境
框架 版本
Spring Boot                 2.0.3.RELEASE
Spring Cloud Finchley.RELEASE
redis redis-4.0.11
JDK 1.8.x

 

前置准备工作

  1. 本机安装一个 redis ,端口按默认的,然后启动。
  2. 创建一个 eureka-service ,端口是 8888,然后启动。
  3. 父工程pom文件,滑动滚轮即可看到pom 的内容。

这里的 父pom 除上文已有的依赖之外,只需要再添加如下即可


            redis.clients
            jedis
        

 

核心代码示例

首先我们创建一个本次核心的工程,这个工程完全可以是你们项目里公共工程的其中一个文件夹,但在我的 Demo 中,我这个核心的工程起名叫 redis-tool。

我们准备一个 lua 脚本,把以下代码复制 ,粘贴到 redis-tool 项目中的 resources 目录下,起名 lock.lua 即可。

解锁 lua 脚本

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

脚本解释:

这个脚本是解锁时使用的,参数KEYS[1]赋值为第一个参数,是要解锁的 key,ARGV[1]赋值为requestId。

 

Redis配置类

该配置类叫  RedisConfig ,继上文(https://blog.csdn.net/weixin_38003389/article/details/89049135)进行改造。

@Configuration
public class RedisConfig {
    private Logger logger = LoggerFactory.getLogger(RedisConfig.class);
    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;

    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;

    @Value("${spring.redis.jedis.pool.max-wait}")
    private long maxWaitMillis;



    /**
     * 创建一个redisPool对象
     *
     * @return
     */
    @Bean
    public JedisPool redisPoolFactory() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        jedisPoolConfig.setMaxTotal(maxActive);
        jedisPoolConfig.setMinIdle(minIdle);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);

        logger.info("JedisPool注入成功!");
        logger.info("redis地址:" + host + ":" + port);
        return jedisPool;
    }

    /**
     * 读取限流lua脚本
     *
     * @return
     */
    @Bean
    public DefaultRedisScript redisluaScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }

    /**
     * 读取解锁lua脚本
     *
     * @return
     */
    @Bean
    public DefaultRedisScript redisLockLuaScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }

    /**
     * redis序列化
     * @param connectionFactory
     * @return
     */
    @Bean
    public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }

}

 

 

加锁正确姿势

@Component
public class RedisLock {
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    @Autowired
    private JedisPool jedisPool;
    @Autowired
    private DefaultRedisScript redisLockLuaScript;

    /**
     * 尝试获取分布式锁
     *
     * @param lockKey    锁
     * @param requestId  请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
        Jedis jedis = jedisPool.getResource();
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }
}

 

代码解释:

我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。

  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

小结:执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

一步加锁操作可以保证要么加锁成功,要么加锁失败。

 

解锁正确姿势

/**
     * 释放分布式锁
     *
     * @param lockKey   锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public boolean releaseDistributedLock(String lockKey, String requestId) {
        Jedis jedis = jedisPool.getResource();
        Object result = jedis.eval(redisLockLuaScript.getScriptAsString(), Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

 

大家对解锁操作因该没什么疑问,就是执行一个 脚本而已。

 

测试

伪集群的方式测试多个请求同时 加锁和解锁,创建一个 eureka 的客户端,在main 方法中操作,代码如下:

@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(value = {"com.annotaion", "cn.springcloud", "com.config", "com.redislock"})

public class Ch34EurekaClientApplication implements ApplicationRunner {
    private static final ExecutorService executorService = Executors.newFixedThreadPool(5);
    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
    private static final String uuid = UUID.randomUUID().toString();
    @Autowired
    RedisLock redisLock;

    public static void main(String[] args) {
        SpringApplication.run(Ch34EurekaClientApplication.class, args);

    }

    @Override
    public void run(ApplicationArguments args) throws Exception {

        for (int i = 0; i < 5; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + "开始等待其他线程");
                        cyclicBarrier.await();
                        System.out.println(Thread.currentThread().getName() + "线程就位,即将同时执行");
                        boolean result = redisLock.tryGetDistributedLock("lock", uuid, 1000);
                        if (result) {
                            System.out.println(Thread.currentThread().getName() + "获取成功,并开始执行业务逻辑");
                            result = redisLock.releaseDistributedLock("lock", uuid);
                            if (result) {
                                System.out.println(Thread.currentThread().getName() + "释放成功");
                            }
                        } else {
                            System.out.println(Thread.currentThread().getName() + "获取失败");
                        }

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }

                }
            });
        }
        executorService.shutdown();
    }
}

 

以下是必要的控制台日志输出

Redis实现分布式锁的正确方式_第1张图片

 

总结:

以上就是 单机 redis 实现分布式锁的正确姿势,如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁。

参考文章:http://www.cnblogs.com/linjiqin/p/8003838.html

https://juejin.im/post/5ba0a098f265da0adb30c684

本文工程结构:

Redis实现分布式锁的正确方式_第2张图片

 

 

Redis实现分布式锁的正确方式_第3张图片Redis实现分布式锁的正确方式_第4张图片

 

你可能感兴趣的:(java)