基于Redis的分布式锁

最近接个需求,要求生成全局id。在分布式环境下,需要用到分布式锁。实现思路在网上搜了一下,看到几个博文讲的特别好。大体意思是指不能单纯使用SETNX实现,会有一些隐患。大家都推荐使用LUA脚本。

首先要确认Redis的版本,一定要高于2.6.0。

EVAL and EVALSHA are used to evaluate scripts using the Lua interpreter built into Redis starting from version 2.6.0.

LUA脚本的特性之一如下:

 

Atomicity of scripts

Redis uses the same Lua interpreter to run all the commands. Also Redis guarantees that a script is executed in an atomic way: no other script or Redis command will be executed while a script is being executed. This semantic is similar to the one of MULTI / EXEC. From the point of view of all the other clients the effects of a script are either still not visible or already completed.
However this also means that executing slow scripts is not a good idea. It is not hard to create fast scripts, as the script overhead is very low, but if you are going to use slow scripts you should be aware that while the script is running no other client can execute commands.

翻译过来如下: 

脚本的原子性

redis 使用相同的 lua 解释器来运行所有命令。redis 还保证以原子方式执行脚本: 在执行脚本时不会执行任何其他脚本或 redis 命令。这个语义类似于 muti/exec。从所有其他客户端的角度来看, 脚本的效果要么仍然不可见, 要么已经完成。
但是, 这也意味着执行慢速脚本不是一个好主意。创建快速脚本并不难, 因为脚本开销很低, 但如果要使用慢速脚本, 则应注意, 在脚本运行时, 没有其他客户端可以执行命令。

根据原子特性,编写分布式锁优势蛮多的。下面是我的实现,因为对 Spring 不是很感冒,所以浪费了一些时间。

代码里几个时间比较重要,需要注意一下。

1. key的过期时间,在公司的业务场景,设置的是30秒。

2. 超时时间,超时时间推荐的是5秒。

3. 间隔请求时间,目前设置的是1秒,在有些业务场景可能不是太适用,或者可以缩短休眠时间。也没想到太好的办法。

package xx;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import redis.clients.jedis.Jedis;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName DistributedLock
 * @Description 基于 Redis 实现的分布式锁
 * @Author Tom
 * @Date 2019/1/4 14:35
 * @Version 1.0
 **/
@Component
public class RedisDistributedLock {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     *
     * @param key
     * @param value
     * @param expireTime
     * @param timeout 超时时间
     * @return
     * @throws Exception
     */
    public boolean tryLock(String key, String value, long expireTime, long timeout) throws Exception {

        long timeoutNanoTime = TimeUnit.SECONDS.toNanos(timeout);
        long start = System.nanoTime();
        long past = 0L;
        boolean lock = false;
        do {
            Jedis jedis = getJedis(redisTemplate);

            String scriptStr = "if redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX') then return 1 else return 0 end";
            Object resultObj = jedis.eval(scriptStr, 1, key, value, String.valueOf(expireTime));

            if(resultObj == null) {
                throw new Exception("try lock failed");
            }
            Long result = (Long)resultObj;

            int intValue = result.intValue();
            if(intValue == 1) {
                lock = true;
                break;
            }

            if(intValue == 0) {
                // 这里要休眠一秒,不是太合理,如果大家有什么好办法,请告知我
                TimeUnit.SECONDS.sleep(1);
                past = System.nanoTime() - start;
                timeoutNanoTime -= past;
            }
        } while (timeoutNanoTime > 0);

        return lock;
    }

    /**
     *
     * @param key
     * @param value
     * @return
     * @throws Exception
     */
    public boolean release(String key, String value) throws Exception {

        String scriptStr = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 2 end";
        Jedis jedis = getJedis(redisTemplate);

        Object object = jedis.eval(scriptStr, 1, key, value);
        if(object == null) {
            throw new Exception("release failed, key is " + key);
        }

        Long result = (Long)object;
        if(result == 1) {
            return true;
        }

        return false;
    }

    private Jedis getJedis(RedisTemplate redisTemplate) {

        RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
        Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis");
        ReflectionUtils.makeAccessible(jedisField);
        Jedis jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection());

        return jedis;
    }
}

踩的几个坑:

1. RedisTemplate方法的SET key value [EX seconds] [PX milliseconds] [NX|XX]没有返回值,其实我也不确定是不是Redis提供的原声API。总之不好用。

2. jedis.eval方法返回值是Long类型。

3. RedisTemplate的序列化方法相当恶心。执行脚本的时候参数相当严格。

 

相对于EVAL方法,还有一个EVALSHA。等有时间了优化一下代码。

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值