通过Redis 实现分布式锁_利用Jedis 客户端

橙三吉。 提交于 2019-12-19 03:07:24

前言

分布式锁一般有三种实现方式:

  1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。

本篇博客将介绍第二种方式,基于Redis实现分布式锁。

虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。

可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

互斥性:在任意时刻,只有一个客户端能持有锁。
不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

一、引入redis 依赖

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

备注:根据版本不同,jedis 的set 方法也有所不同

二、配置文件增加redis 配置

# redis.properties 配置文件:

# region Redis jedis
# redis配置开始

# Redis数据库索引(默认为0)
spring.redis.database=0

# Redis服务器地址
spring.redis.host=localhost

# Redis服务器连接端口
spring.redis.port=6379

# Redis服务器连接密码(默认为空)
spring.redis.password=redis123456

# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=1024

# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=10000

# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=100

# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=5

# 连接超时时间(毫秒)
spring.redis.timeout=10000

# 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时
spring.redis.block-when-exhausted=true

# endregion

三、利用Spring 把JedisPool 加入Bean 工厂

@Configuration
@PropertySource("classpath:application.properties")
@Slf4j
public class RedisConfig {

    @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-idle}")
    private int maxIdle;

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

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.block-when-exhausted}")
    private boolean blockWhenExhausted;

    @Bean
    public JedisPool redisPoolFactory() throws Exception {
        log.info("JedisPool注入开始!!");
        log.info("redis地址:" + host + ":" + port);
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        // 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
        jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
        // 是否启用pool的jmx管理功能, 默认true
        jedisPoolConfig.setJmxEnabled(true);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
        log.info("JedisPool注入成功!!");
        return jedisPool;
    }
}

四、封装RedisService 对外提供服务

@Service
@Slf4j
public class RedisService {

    //Redis 成功返回结果标识
    private static final String LOCK_SUCCESS = "OK";
    //Reis 操作返回成功
    private static final Long RELEASE_SUCCESS = 1L;
    //Redis锁不存在时才设置成功
    private static final String SET_IF_NOT_EXIST = "NX";
    //Redis锁超时时间单位
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    @Autowired
    private JedisPool jedisPool;

    /**
     * 尝试获取分布式锁
     *
     * @param lockKey    锁
     * @param requestId  请求唯一标识(可以通过uuid等方式获取唯一ID)
     * @param expireTime 超期时间(毫秒)
     * @return 是否获取成功
     */
    public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {

        Jedis jedis = jedisPool.getResource();
        
        //Jedis 3.0.0 以前的版本
        //String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        
        //Jedis 3.0.0 及以后的版本
        SetParams setParams = SetParams.setParams().nx().px(expireTime);
        String result = jedis.set(lockKey, requestId, setParams);

        return LOCK_SUCCESS.equals(result);
    }

    /**
     * 释放分布式锁
     *
     * @param lockKey   锁
     * @param requestId 请求唯一标识(加锁时用的唯一标识)
     * @return 是否释放成功
     */
    public boolean releaseDistributedLock(String lockKey, String requestId) {

        Jedis jedis = jedisPool.getResource();

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        return RELEASE_SUCCESS.equals(result);
    }
}

五、简单解释

1. 利用redis 的set 命令的 5个参数保证操作的原子性

2. 利用Lua 脚本保证在释放锁时的原子性

3. 利用requestId 唯一标识保证不会释放别人的锁
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!