专栏首页晏霖Redis实现分布式锁的正确方式

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

封面为好友拍摄的照片,想查看更多微信公众号搜索:JavaBoy王皓或csdn博客搜索:TenaciousD

前言

上一篇文章讲的是 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 除上文已有的依赖之外,只需要再添加如下即可

<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

核心代码示例

首先我们创建一个本次核心的工程,这个工程完全可以是你们项目里公共工程的其中一个文件夹,但在我的 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<Number> redisluaScript() {
        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }

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

    /**
     * redis序列化
     * @param connectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Serializable> 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<Number> 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 实现分布式锁的正确姿势,如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁。

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

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

本文工程结构:

本文分享自微信公众号 - 晏霖(yanlin199507),作者:晏霖

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-04-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Redis集群实现布隆过滤器

    封面为好友拍摄的照片,想查看更多微信公众号搜索:JavaBoy王皓或csdn博客搜索:TenaciousD

    胖虎
  • SpringBoot正确、安全地关闭服务

    正文 本文依然使用v1.5.8.RELEASE ,讲地是利用actuator的Endpoints实现关闭服务

    胖虎
  • 【Nacos系列第一篇】-Nacos之Spring Discovery

    个人比较看好Spring Cloud Alibaba家族。此系列以Nacos为主题,从Spring、Spring boot、Spring Cloud多个...

    胖虎
  • 初学redis之windows服务配置与启动

    初学redis首先要配置好服务, redis在Linux上的安装只要按照官方指导来,很快很简单。 下面来谈谈redis在windows上的安装。 官网虽然没给r...

    lonelydawn
  • redis-cluster配置

    一台服务器内存正常是16~256G,假如你的业务需要500G内存,你怎么办?解决方案如下

    超蛋lhy
  • 简单的redis缓存操作(get、put)

    本文介绍简单的redis缓存操作,包括引入jedisjar包、配置redis、RedisDao需要的一些工具、向redis中放数据(put)、从redis中取数...

    用户2409797
  • vue项目在安卓低版本机显示空白原因

    查看安卓debug,报错,可能有箭头函数语法错误,或者其他语法问题,那可能是ES6语法问题。

    蓓蕾心晴
  • ssh key类型这么多,要如何选择呢?

    用过ssh的朋友都知道,ssh key的类型有很多种,比如dsa、rsa、 ecdsa、ed25519等,那这么多种类型,我们要如何选择呢?

    wangyuntao
  • [Effective Modern C++(11&14)]Chapter 6:Lambda Expressions

    昊楠Hacking
  • 【应急响应】redis未授权访问致远程植入挖矿脚本(完结篇)

    aerfa

扫码关注云+社区

领取腾讯云代金券