Redis高级用法

 

Redis6.0已经低调的发布了稳定版,最大的变化就是支持I/O多线程,但旧版本就真的是单线程吗,事情往往不是这么简单,

这里的单线程指的是只有一个执行线程串行处理命令,再配合多路复用机制,实际上数据持久化、主从同步、连接释放等都

有其他线程来处理。既然6.0都出来了,之前的文章我也说了不少Redis相关的了,借此契机,我们就来看看Redis的一些高级用法吧。

 

作者原创文章,谢绝一切形式转载,违者必究!

 

准备:

Idea2019.03/JDK11.0.4/Jedis3.3.0/Redisson3.12.5

难度: 新手--战士--老兵--大师

目标:

  1. Pipeline管道的使用
  2. 位图bitmap使用
  3. Redis红锁原理
  4. Redis事务
  5. Lua脚本

1 Pipeline

Redis-cli有命令mset/mget,hmset/hmget,可以一次批处理多个赋/取值命令,那么Java API有Pipeline(管道)进行批处理,

可以将多个命令放入Pipeline,并一次获得全部的执行结果,与client交互只有一个来回,示意图如下:

我们来一段测试代码:

/** compile group: 'redis.clients', name: 'jedis', version: '3.3.0' */
public class RedisTest {
    public static void main(String[] args) {
        Jedis redis = new Jedis("127.0.0.1", 6379);
        //使用第1个库
        redis.select(1);
        redis.flushDB();
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 50000; i++) {
            redis.set("key_"+i,"v_"+i);
            redis.get("key_"+i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("formal time used >>> " + (endTime - startTime) + " ms");
        //
        startTime = System.currentTimeMillis();
        Pipeline pipeline = redis.pipelined();
        for (int i = 0; i < 50000; i++) {
            // 返回Response<T>,但这里还未直接返回给client
            pipeline.set("key_2"+i,"v_2"+i);
            // 返回Response<T>,但这里还未直接返回给client
            pipeline.get("key_2" + i);
            /**这里使用打印输出会出错*/
            //Response<String> response = pipeline.get("key_2" + i);
            //System.out.println(response.get());
        }
        // Synchronize pipeline by reading all responses. This operation close the pipeline
        // 这里将同步pipeline所有返回结果,并放入List,返回void
        pipeline.sync();
        // 如果需要返回结果集,可以使用以下方法,但官方建议应尽量避免使用,因为需要对pipeline所有返回结果做同步,很耗时,
        //List<Object> returnAll = pipeline.syncAndReturnAll();
        //System.out.println("completed commands >>> "+returnAll.size());
        //returnAll.forEach(System.out::println);
        endTime = System.currentTimeMillis();
        System.out.println("pipeline time used >>> " + (endTime - startTime) + " ms");
    }
}
 

运行结果,可见Pipeline十分省时:

注意:高频命令场景下,应避免使用管道,因为需要先将全部执行命令放入管道,会耗时。另外,需要使用返回值的情况也不建议使用,

同步所有管道返回结果也是个耗时的过程!管道无法提供原子性/事务保障。

2 分布式锁

首先,一个好的分布式锁,应该具有以下特征:

  • 互斥——同时刻只能有一个持有者;
  • 可重入——同一个持有者可多次进入;
  • 阻塞——多个锁请求者时,未获得锁的阻塞等待;
  • 无死锁——持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取;
  • 容错——只要大部分节点正常,仍然可以获取和释放锁;

单Redis实例下,通过SETNX命令来实现Redis分布式锁,已不推荐使用,如果说使用SET命令配合NX参数或许会让面试官更为满意,

因为SET命令可带有EX/PX/NX/XX参数,更为强大,可以完成更复杂业务,但这些在此不表,我想说的是分布式多实例下的红锁算法:

在Redis的分布式环境中,我们假设有N(建议为5)个Redis master,这些节点完全互相独立,不存在主从复制或者其他集群协调机制。

获取一个红锁的步骤如下:

  1. 获取当前系统时间,毫秒为单位
  2. 依次(同时并发地)尝试从N个实例,使用相同的Key和随机值(全局唯一)获取锁
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)即为获取锁消耗的时间。当且仅当从大多数(N/2+1)的Redis节点都获取到锁,并且消耗时间小于锁失效时间时,红锁才算获取成功
  4. 如果获取到了红锁,key真正的有效时间等于有效时间减去获取锁消耗的时间(步骤3中的计算结果)
  5. 如果获取红锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间超过了锁的有效时间),客户端应该在所有的Redis实例上进行解锁(即使某些Redis实例根本就没有加锁成功)

A. 锁指单Redis实例上使用如SET命令获取的锁,红锁是将多个单Redis实例锁组合为一个锁来管理,从而避免单点故障

B. 步骤2中,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,

则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,

客户端应该尽快尝试另外一个Redis实例。

C. 当客户端无法取到红锁时,应该在一个随机延迟后重试,防止多个客户端在同时抢夺同一资源的锁(这样会导致脑裂,没有人会取到锁),

并且当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,这样其他的客户端就不必非得等到锁过完“有效时间”才能取到,

Redission实现的红锁:

/** compile group: 'org.redisson', name: 'redisson', version: '3.12.5' */
public class RedLockTest {
    public static void main(String[] args) throws InterruptedException {
        Config config = new Config();
        // 集群模式
        config.useClusterServers()
                .addNodeAddress("127.0.0.1:6379")
                .addNodeAddress("127.0.0.1:6380")
                .addNodeAddress("127.0.0.1:6381");
        RedissonClient redisson = Redisson.create(config);
        // RLock:Redis based implementation of {java.util.concurrent.locks.Lock}
        RLock lock1 = redisson.getLock("lock1");
        RLock lock2 = redisson.getLock("lock2");
        RLock lock3 = redisson.getLock("lock3");
        // 严格来讲,此处多个RLock应该从独立的Redis实例上获取,再组合为RedLock
        // 将多个独立的RLock组合为一个RedLock
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        // lock实际上调用的是有默认值的tryLock,
        // 返回void
        redLock.lock();
        // 异步请求锁,返回RFuture
        redLock.lockAsync();
        // 返回Boolean
        redLock.tryLock(10L,5L,TimeUnit.SECONDS);
        try{
            // 业务逻辑处理
            System.out.println("RedLock acquire success.");
        }finally {
            redLock.unlock();
        }
    }
}
 

以上代码解析:RLock是实现了java.util.concurrent.locks.Lock接口而基于Redis 的锁,即单Redis实例的锁,RedissonRedLock即红锁,使用上就和JUC下的Lock类似了。

3 位图

Redis中有一种特殊的存储类型,二进制位对象——位图(bitmap),存储上是按照”字符串”处理,如果看官知晓布隆过滤器,就应该十分熟悉位图了,其特点很明显,

就是每位只有0/1两种状态值,如下图示例,然后就是体积小,10M可存储8千万bit位,最大允许512M,可存储40亿bit位。

Redis-cli中只有6个命令:

  • SETBIT:设置或清除指定偏移量上的位(bit)。当 key 不存在时,自动生成一个新的字符串值。字符串会进行伸展(grown)以确保它可以将 value 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充。
  • GETBIT:对 key 所储存的字符串值,获取指定偏移量上的位(bit)。当 offset 比字符串值的长度大,或者 key 不存在时,返回 0。
  • BITCOUNT:计算给定字符串中,被设置为 1 的比特位的数量。对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。
  • BITPOS:返回位图中第一个值为 bit 的二进制位的位置。
  • BITTOP:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
  • BITFIELD:将一个 Redis 字符串看作是一个由二进制位组成的数组, 并对这个数组中储存的长度不同的整数进行访问。

RedisCli下使用,略!Jedis API模式下,核心部分代码如下,其他辅助代码同上:

...
Jedis redis = new Jedis("127.0.0.1", 6379);
redis.setbit("bit_key",1000L, "1");
redis.getbit("bit_key",1000L);
redis.bitcount("bit_key",1L,1500L);
// 对"bit_key1","bit_key2"做AND位运算,并保存到"dest_key"中
redis.bitop(BitOP.AND,"dest_key","bit_key1","bit_key2");
// redis.bitpos("bit_key",false);
redis.bitpos("bit_key",true);
...
 

适用场景:比如记录用户是否登录,登录的次数统计等;当然,实现布隆过滤器肯定是可以的。

4 事务

Redis使用MULTI标记一个事务块的开始。 事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。

Redis-cli下使用如下图:

Jedis API使用核心部分代码如下,辅助代码,略:

...
Jedis redis = new Jedis("127.0.0.1", 6379);
// 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
redis.watch("k1","k2","k3");
Transaction transaction = redis.multi();
// 事务内多个命令
transaction.setbit("bit_key",100L, Boolean.parseBoolean("1"));
transaction.mset("k1","v1","k2","v2","k3","v3");
List<Object> list = transaction.exec();
// 放弃事务
//String discard = transaction.discard();
/**如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了
 * 因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会
 * 取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了*/
redis.unwatch();
...
 

5 Lua脚本

Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,

不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。

Redis-cli模式,命令行格式:

EVAL script numkeys key [key …] arg [arg …]

Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问键名参数;不是键名参数的附加参数 arg [arg ...] ,可以在 Lua 中通过全局变量 ARGV 数组访问。

在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:redis.call()和redis.pcall(),这两个函数的唯一区别在于它们使用不同的方式处理

执行命令所产生的异常。对于Lua脚本,还有SCRIPT 命令进行管理,在此不表。

Jedis API使用核心部分代码如下,辅助代码略:

...
Jedis redis = new Jedis("127.0.0.1", 6379);
List<String> keyList = Arrays.asList("key01","key01");
List<String> argsList = Arrays.asList("value01","value02");
// eval(scripts,keyList,argsList)
redis.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", keyList, argsList );
// eval(scripts,keyCount,...params)
redis.eval( "return redis.call('set',KEYS[1],'bar')", 1, "foo");
...
 

6 新特性

最后还是说下新Redis6.0,最大变化是把网络读写改为多线程,5.0版本使用IO多路复用,6.0改为多线程与Socket绑定,

但是注意IO 多线程要么同时在读,要么同时在写,不会同时读和写,且IO 线程只负责读写 socket、 解析命令,不负责命令执行,

从这个角度看6.0还是单线程的!

IO 多线程的目的一是更充分的利用多核CPU资源,低版本只能使用一个核,二是提高同步IO读写负载。在设置时,IO线程数一

定要小于机器核数。实际上,只有你的机器Redis实例已经占用相当大的CPU耗时的时候才建议采用,否则使用多线程没有意义,

所以此新特性很多时候我们也用不上!

 

全文完!


近期其他文章:

      只写原创,敬请关注 

posted @ 2020-05-10 10:55  甲由崽  阅读(2407)  评论(0编辑  收藏  举报