前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis实现分布式锁的正确方式

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

作者头像
胖虎
发布2019-06-26 17:13:35
7910
发布2019-06-26 17:13:35
举报
文章被收录于专栏:晏霖晏霖

封面为好友拍摄的照片,想查看更多微信公众号搜索: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 除上文已有的依赖之外,只需要再添加如下即可

代码语言:javascript
复制
<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

核心代码示例

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

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

解锁 lua 脚本

代码语言:javascript
复制
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)进行改造。

代码语言:javascript
复制
@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;
    }

}
代码语言:javascript
复制

加锁正确姿势

代码语言:javascript
复制
@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. 已有锁存在,不做任何操作。

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

解锁正确姿势

代码语言:javascript
复制
/**
     * 释放分布式锁
     *
     * @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;
    }
代码语言:javascript
复制

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

测试

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

代码语言:javascript
复制
@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();
    }
}
代码语言:javascript
复制

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

总结:

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

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

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

本文工程结构:

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 晏霖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • 前置准备工作
      • 核心代码示例
        • 加锁正确姿势
          • 解锁正确姿势
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档