Redis + lua 分布式锁、分布式限流

4,051 阅读4分钟

Redis + Lua

  1. 使用lua脚本的好处:
    • 原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入,换句话说,编写脚本的过程中无需担心会出现插队、竞争等条件;
    • 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行;

在Redis中使用Lua脚本

  1. 我们可以编写Lua脚本,然后再Lua脚本中调用Redis命令,使用redis.call函数调用。
  2. 比如调用string类型的命令:
    • eval "return redis.call('set', 'k1', 'v1')" 0 // OK
    • eval "return redis.call('get','k1')" 0
    • eval "return redis.call('get', KEYS[1])" 1 K2 // 使用后置传参方式
127.0.0.1:6379> eval "return redis.call('set', 'name', 'zhangsan')" 0
OK

127.0.0.1:6379> eval "return redis.call('get', 'name')" 0
"zhangsan"

// jedis 使用方式
Jedis jedis = JedisPoolUtil.getInstance().getResource();
Object o = jedis.eval("return redis.call('get','name')");

EVALSHA命令

  1. 通过 script load "return redis.call('get','k1')" ,返回sha值:97a8c622c96716f456b9b29b31243e52eeafb1c2
  2. EVALSHA 97a8c622c96716f456b9b29b31243e52eeafb1c2 0
  3. 考虑到我们通过eval执行lua脚本,脚本比较长的情况下,每次调用脚本都需要把整个脚本传给redis,比较占用带宽,为了解决这个问题,redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本。
        Jedis jedis = JedisPoolUtil.getInstance().getResource();
        // 52da8c7de39385e305fb1af2a8ffd21534af996f
        String s = jedis.scriptLoad("return redis.call('get','name')");
        Object o = jedis.evalsha(s);  // zhangsan
        System.out.println("o = " + o);

Redis + Lua 实现分布式锁

  1. 依赖于redis里提供了SETNX互斥特性的命令
    • SETNX:在Key不存在的情况下才会给 Key 设置值成功,否则返回0
    • EXPIRE:设置过期时间,过期后自动删除key
    • DEL :主动删除Key(释放锁)
  2. 主要使用以上3个命令实现分布式锁, 首先尝试使用 SETNX 设置值,如设置成功则等同于获取到锁,而其他线程不可能再设置成功,释放锁手动删除Key
  3. jedis 实现分布式锁
  4. lua 语言加锁
if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then
    --设置成功返回1,当key不存在或者不能为key设置生存时间时,返回0
    return redis.call('expire', KEYS[1], ARGV[2]);
else
--没有获取到锁
    return 0;
end
  1. lua 语言解锁
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    --被删除key的数量
    return redis.call('del', KEYS[1]);
else
    return 0;
end
  1. jedis 代码实现
public class LuaJedisLock {

    private String key;
    /**
     * 每一个锁的 key,不同的业务使用不同的key
     *
     * @param key
     */
    public LuaJedisLock(String key) {
        this.key = "redis:lock:" + key;
    }

    /**
     * 获取锁
     *
     * @param timeout     获取锁等待的超时间
     * @param processTime 处理过程中的超时时间
     * @return
     */
    public boolean acquireLock(long timeout, long processTime) {

        Jedis jedis = null;
        try {
            jedis = JedisPoolUtil.getInstance().getResource();

            // 使用SETNX 互斥命令 设置 key值,如果设置成功了,返回1
            // 并且 设置超时时间, 为锁的最大处理时间, 设置成功也返回1
            StringBuilder sb = new StringBuilder()
                    .append("if (redis.call('SETNX', KEYS[1], ARGV[1]) == 1) then    ")
                    .append("return redis.call('expire', KEYS[1], ARGV[2])           ")
                    .append("else                                                    ")
                    .append("return 0                                                ")
                    .append("end                                                     ");
            // 转换为 EVALSHA 命令执行
            String sha1 = jedis.scriptLoad(sb.toString());

            // 获取锁的最后时间, 否则则超时获取不到
            long endTime = System.currentTimeMillis() + timeout;

            // 当前时间 小于获取的超时时间,可以持续获取
            while (System.currentTimeMillis() < endTime) {

                Long result = (Long) jedis.evalsha(sha1, 1, key, "lockValue", String.valueOf(processTime));
                if (result == 1) return true;
                // 这里说明没有获取到锁,休眠一会,继续循环
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return false;
    }

    /**
     * 释放锁, 就是删除 这个锁的key
     */
    public void releaseLock() {
        Jedis jedis = null;
        try {
            jedis = JedisPoolUtil.getInstance().getResource();
            jedis.del(key);
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

分布式锁的不足

  1. 当加入分布式锁后,实际上就变成了单线程执行了,面对秒杀等较大QPS时,会很慢
  2. 可以采用,如将库存 分段 处理,不同的 段 使用 不同的分布式锁,使用不同的策略去访问不同段的数据,再加锁。
  3. 分段锁优化思想;
  4. 1000个库存,分成50段,每段20个库存;
  5. 随机或轮询算法定位分段;
  6. 支持同时50个并发下单;
  7. 假设每个下单耗时50ms;
  8. 50ms下单50;
  9. 1000ms下单=1000/50=20*50=1000个

Redis + Lua实现分布式限流

  1. 在请求访问时,先访问redis获取是否达到 当前单位时间内最大访问次数了
  2. 如果达到则直接返回,不再继续执行,如果没有 增加1访问次数,继续执行
  3. 是1秒内允许放行多少个请求,下一秒放行同样多的请求进来
public class LimitService {
    /**
     * 可以限制 指定的key 在一定时间内的 访问次数
     * @param key
     * @param expireTime
     * @return
     */
    public static Long limit2(String key, int expireTime) {

        Jedis jedis = null;

        try {
            jedis = JedisPoolUtil.getInstance().getResource();

            StringBuilder sb = new StringBuilder()
                    .append("local key1 = KEYS[1]                        ")
                    .append("local expireTime = ARGV[1]                  ")
                    .append("local count = redis.call('incr', key1)      ")
                    .append("if (redis.call('ttl', key1) == -1) then     ")
                    .append("redis.call('expire',key1, expireTime)       ")
                    .append("end                                         ")
                    .append("return count                                ");

            String sha1 = jedis.scriptLoad(sb.toString());
            Long count = (Long) jedis.evalsha(sha1, 1, key, String.valueOf(expireTime));
            return count;

        } finally {

            if (jedis != null) {
                jedis.close();
            }
        }
    }
}
  1. 使用方式
        for(;;) {
            Long cout = LimitService.limit2("duweiwu", 1);
            if (cout <= 10) {
                System.out.println("请求被处理...");
            } else {
                System.out.println("请求被限流, 对不起网络开小差了...");
            }
            Thread.sleep(50);
        }