2020重新出发,NOSQL,Redis的常用数据类型

@

Redis的数据类型

Redis 是一种基于内存的数据库,并且提供一定的持久化功能,它是一种键值数据库,使用 key 作为索引找到当前缓存的数据,并且返回给程序调用者。

当前的 Redis 支持 5 种基础数据类型和 3 种特殊数据类型,它们分别是字符串(String)、哈希结构(hash)、列表(List)、集合(set)、有序集合(zset)和基数(HyperLogLog)、GEO(geospatial)、位图(bitmap)。

Redis 定义的这数据类型是十分有用的,它除了提供简单的存储功能,还能对存储的数据进行一些计算。

比如字符串可以支持浮点数的自增、自减、字符求子串,集合求交集、并集,有序集合进行排序等,所以使用它们有利于对一些不太大的数据集合进行快速计算,简化编程,同时它也比数据库要快得多,所以它们对系统性能的提升十分有意义。

  • STRING(字符串):可以是保存字符串、整数和浮点数 ,可以对字符串进行操作
    • 比如增加字符或者求子串:
    • 如果是整数或者浮点数,可以实现计算,比如自增等
  • LIST(列表):它是一个链表,它的每一个节点都包含一个字符串
    • Redis 支持从链表的两端插入或者弹出节点,或者通过偏移对它进行裁剪;
    • 还可以读取一个或者多个节点,根据条件删除或者查找节点等
  • SET(集合):它是一个收集器,但是是无序的,在它里而每一个元素都是一个字符串,而且是独一无二,各不相同的
    • 可以新增、读取、删除单个元素:检测一个元素是否在集合中;
    • 计算它和其他集合的交集、并集和差集等;
    • 随机从集合中读取元素
  • HASH(哈希散列表):它类似于 Java 语言中的 Map,是一个键值对应的无序列表
    • 可以増、删、査、改单个键值对,也可以获取所有的键值对
  • ZSET(有序集合):它是一个有序的集合,可以包含字符 串、整数、浮点数、分值(score),元素 的排序是依据分值的大小来决定的
    • 可以增、删、査、改元素,根据分值的范围或者成员 來获取对应的元索
  • geospatial(Geo):Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离。
    • 规则:两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。
    • 有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。当坐标位置超出指定范围时,该命令将会返回一个错误。
  • HyperLogLog(基数):它的作用是计算重复的值,以确定存储的数量
    • 只提供基数的运算,不提供返回的功能
  • bitmap (位图):是通过最小的单位bit来进行0或者1的设置,表示某个元素对应的值或者状态。一个bit的值,或者是0,或者是1;也就是说一个bit能存储的最多信息是2。
    • bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等。

String 数据结构和常用命令

字符串(String)是 Redis 最基本的数据结构,它将以一个键和一个值存储于 Redis 内部,它犹如 Java 的 Map 结构,让 Redis 通过键去找到值。Redis 字符串的数据结构如图 1 所示。

Redis字符串数据结构

​ 图 1 Redis 字符串数据结构

Redis 会通过 key 去找到对应的字符串,比如通过 key1 找到 value1,又如在 Java 互联网中,假设产品的编号为 0001,只要设置 key 为 product_0001,就可以通过 product_0001 去保存该产品到 Redis 中,也可以通过 product_0001 从 redis 中找到产品信息。

常用命令如下:

命 令 说 明 备 注
set key value 设置键值对 最常用的写入命令
get key 通过键获取值 最常用的读取命令
del key 通过 key,删除键值对 删除命令,返冋删除数,注意,它是个通用的命令,换句话说在其他数据结构中,也可以使用它
strlen key 求 key 指向字符串的长度 返回长度
getset key value 修改原来 key 的对应值,并将旧值返回 如果原来值为空,则返回为空,并设置新值
getrange key start end 获取子串 记字符串的长度为 len,把字符串看作一个数组,而 Redis 是以 0 开始计数的,所以 start 和 end 的取值范围 为 0 到 len-1
append key value 将新的字符串 value,加入到原来 key 指向的字符串末 返回 key 指向新字符串的长度

为了让大家更为明确,在 Redis 提供的客户端进行测试如图 2 所示。

Redis操作字符串重用命令

​ 图 2 Redis 操作字符串重用命令

这里我们看到了字符串的常用操作,为了在 Spring 中测试这些命令,首先配置 Spring 关于 Redis 字符串的运行环境,配置 Spring 关于 Redis 字符串的运行环境代码如下所示。

<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxIdle" value="50" />
    <property name="maxTotal" value="100" />
    <property name="maxWaitMillis" value="20000" />
</bean>

<bean id="connectionFactory"
    class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <property name="hostName" value="localhost" />
    <property name="port" value="6379" />
    <property name="poolConfig" ref="poolConfig" />
</bean>
<bean id="jdkSerializationRedisSerializer"
    class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
<bean id="stringRedisSerializer"
    class="org.springframework.data.redis.serializer.StringRedisSerializer" />
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    <property name="connectionFactory" ref="connectionFactory" />
    <property name="keySerializer" ref="stringRedisSerializer" />
    <property name="valueSerializer" ref="jdkSerializationRedisSerializer" />
</bean>

注意,这里给 Spring 的 RedisTemplate 的键值序列化器设置为了 String 类型,所以它就是一种字符串的操作。假设把这段 Spring 的配置代码保存为一个独立为文件 applicationContext.xml,使用 Spring 测试 Redis 字符串操作代码如下所示。

package com.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

import com.pojo.Role;

public class Test {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                "applicationContext.xml");
        RedisTemplate redisTemplate = applicationContext
                .getBean(RedisTemplate.class);
        // 设值
        redisTemplate.opsForValue().set("key1", "value1");
        redisTemplate.opsForValue().set("key2", "value2");
        // 通过key获取值
        String value1 = (String) redisTemplate.opsForValue().get("key1");
        System.out.println(value1);
        // 通过key删除值
        redisTemplate.delete("key1");
        // 求长度
        Long length = redisTemplate.opsForValue().size("key2");
        System.out.println(length);
        // 设值新值并返回旧值
        String oldValue2 = (String) redisTemplate.opsForValue().getAndSet(
                "key2", "new_value2");
        System.out.println(oldValue2);
        // 通过key获取值.
        String value2 = (String) redisTemplate.opsForValue().get("key2");
        System.out.println(value2);
        // 求子串
        String rangeValue2 = redisTemplate.opsForValue().get("key2", 0, 3);
        System.out.println(rangeValue2);
        // 追加字符串到末尾,返回新串长度
        int newLen = redisTemplate.opsForValue().append("key2", "_app");
        System.out.println(newLen);
        String appendValue2 = (String) redisTemplate.opsForValue().get("key2");
        System.out.println(appendValue2);
    }
}

这是主要的目的只是在 Spring 操作 Redis 键值对,其操作就等同于图 2 所示的命令一样。

在 Spring 中,redisTemplate.opsForValue() 所返回的对象可以操作简单的键值对,可以是字符串,也可以是对象,具体依据你所配置的序列化方案。

由于配置 Spring 关于 Redis 字符串的运行环境代码所配置的是字符串,所以以字符串来操作 Redis,其测试结果如下:

运行结果

​ 图 3 运行结果

结果和我们看到的命令行的结果一样的,作为开发者要熟悉这些方法。

上面介绍了字符串最常用的命令,但是 Redis 除了这些之外还提供了对整数和浮点型数字的功能。如果字符串是数字(整数或者浮点数),那么 Redis 还能支持简单的运算。不过它的运算能力比较弱,目前版本只能支持简单的加减法运算,如下 。

  • incr key :在原字段上加 1 ,只能对整数操作
  • incrby key increment : 在原字段上加上整数(increment) , 只能对整数操作
  • decr key :在原字段上减 1 ,只能对整数操作
  • decrby key decrement : 在原字段上减去整数(decrement), 只能对整数操作
  • incrbyfloat keyincrement : 在原字段上加上浮点数(increment), 可以操作浮点数或者整数

对操作浮点数和整数进行了测试,如图 4 所示。

操作浮点数和整数

​ 图 4 操作浮点数和整数

在测试过程中,如果开始把 val 设置为浮点数,那么 incr、decr、incrby、decrby 的命令都会失败。Redis 并不支持减法、乘法、除法操作,功能十分有限,这点需要我们注意。

由于 Redis 的功能比较弱,所以经常会在 Java 程序中读取它们,然后通过 Java 进行计算并设置它们的值。

注意,所有关于减法的方法,原有值都必须是整数,否则就会引发异常,

hash 数据结构和常用命令

Redis 中哈希(hash)结构就如同 Java 的 map 一样,一个对象里面有许多键值对,它是特别适合存储对象的,如果内存足够大,那么一个 Redis 的 hash 结构可以存储 2 的 32 次方减 1 个键值对(40 多亿)。

一般而言,不会使用到那么大的一个键值对,所以我们认为 Redis 可以存储很多的键值对。在 Redis 中,hash 是一个 String 类型的 field 和 value 的映射表,因此我们存储的数据实际在 Redis 内存中都是一个个字符串而已。

Redis hash 结构命令,如下。

命 令 说 明 备 注
hdel key field1[field2......] 删除 hash 结构中的某个(些)字段 可以进行多个字段的删除
hexists key field 判断 hash 结构中是否存在 field 字段 存在返回 1,否则返回 0
hgetall key 获取所有 hash 结构中的键值 返回键和值
hincrby key field increment 指定给 hash 结构中的某一字段加上一个整数 要求该字段也是整数字符串
hincrbyfloat key field increment 指定给 hash 结构中的某一字段加上一个浮点数 要求该字段是数字型字符串
hkeys key 返回 hash 中所有的键 ——
hlen key 返回 hash 中键值对的数量 ——
hmget key field1[field2......] 返回 hash 中指定的键的值,可以是多个 依次返回值
hmset key field1 value1 [field2 field2......] hash 结构设置多个键值对 ——
hset key filed value 在 hash 结构中设置键值对 单个设值
hsetnx key field value 当 hash 结构中不存在对应的键,才设置值 ——
hvals key 获取 hash 结构中所有的值 ——

可以看出,在 Redis 中的哈希结构和字符串有着比较明显的不同。

首先,命令都是以 h 开头,代表操作的是 hash 结构。其次,大多数命令多了一个层级 field,这是 hash 结构的一个内部键,也就是说 Redis 需要通过 key 索引到对应的 hash 结构,再通过 field 来确定使用 hash 结构的哪个键值对。

下面通过 Redis 的这些操作命令来展示如何使用它们,如图 2 所示。

Redis的hash结构命令展示

​ 图 2 Redis 的 hash 结构命令展示

从图 2 中可以看到,Redis 关于哈希结构的相关命令。这里需要注意的是:

哈希结构的大小,如果哈希结构是个很大的键值对,那么使用它要十分注意,尤其是关于 hkeys、hgetall、hvals 等返回所有哈希结构数据的命令,会造成大量数据的读取。这需要考虑性能和读取数据大小对 JVM 内存的影响。

对于数字的操作命令 hincrby 而言,要求存储的也是整数型的字符串,对于 hincrbyfloat 而言,则要求使用浮点数或者整数,否则命令会失败。

我们用 Spring 来完成图 2 的功能,代码如下所示。

public static void testRedisHash()  {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    RedisTemplate redisTemplate =  applicationcontext.getBean(RedisTemplate.class);
    String key = "hash";
    Map<String, String> map = new HashMap<String,String>();
    map.put("f1", "val1");
    map.put("f2", "val2");
    // 相当于hmset命令
    redisTemplate.opsForHash().putAll(key, map);
    // 相当于hset命令
    redisTemplate.opsForHash().put(key, "f3", "6");
    printValueForhash (redisTemplate, key, "f3");
    // 相当于 hexists key filed 命令
    boolean exists = redisTemplate.opsForHash().hasKey(key, "f3");
    System.out.println(exists);
    // 相当于hgetall命令
    Map keyValMap = redisTemplate.opsForHash().entries(key);
    //相当于hincrby命令
    redisTemplate.opsForHash().increment(key, "f3",2);
    printValueForhash (redisTemplate, key, "f3");
    //相当于hincrbyfloat命令
    redisTemplate.opsForHash().increment (key, "f3", 0.88);
    printValueForhash(redisTemplate, key, "f3");
    //相当于hvals命令
    List valueList = redisTemplate.opsForHash().values(key);
    //相当于hkeys命令
    Set keyList = redisTemplate.opsForHash().keys(key);
    List<String> fieldList = new ArrayList<String>();
    fieldList.add("f1");
    fieldList.add("f2");
    //相当于hmget命令
    List valueList2 = redisTemplate.opsForHash().multiGet(key, keyList);
    //相当于hsetnx命令
    boolean success = redisTemplate.opsForHash () .putlfAbsent(key, "f4", "val4");
    System.out.println(success);
    //相当于hdel命令
    Long result = redisTemplate.opsForHash().delete(key, "fl", "f2");
    System.out.println(result);   
}
private static void printValueForhash(RedisTemplate redisTemplate,String key,String field) {
    //相当于hget命令
    Object value = redisTemplate.opsForHash().get(key,field);
    System.out.println(value);
}

以上代码做了比较详细的注解,也不难理解,不过需要注意以下几点内容:

  1. hmset 命令,在 Java 的 API 中,是使用 map 保存多个键值对在先的。
  2. hgetall 命令会返回所有的键值对,并保存到一个 map 对象中,如果 hash 结构很大,那么要考虑它对 JVM 的内存影响。
  3. hincrby 和 hincrbyFloat 命令都采用 increment 方法,Spring 会识别它具体使用何种方法。
  4. redisTemplate.opsForHash().values(key) 方法相当于 hvals 命令,它会返回所有的值,并保存到一个 List 对象中;而 redisTemplate.opsForHash().keys(key) 方法相当于 hkeys 命令,它会获取所有的键,保存到一个 Set 对象中。
  5. 在 Spring 中使用 redisTemplate.opsForHash().putAll(key,map) 方法相当于执行了 hmset 命令,使用了 map,由于配置了默认的序列化器为字符串,所以它也只会用字符串进行转化,这样才能执行对应的数值加法,如果使用其他序列化器,则后面的命令可能会抛出异常。
  6. 在使用大的 hash 结构时,需要考虑返回数据的大小,以避免返回太多的数据,引发 JVM 内存溢出或者 Redis 的性能问题。

运行以上代码,可以得到这样的输出结果:

运行结果

​ 图 3 运行结果

操作成功了,按照类似的代码就可以在 Spring 中顺利操作 Redis 的 hash 结构了。

List 数据结构和常用命令

链表(List)结构是 Redis 中一个常用的结构,它可以存储多个字符串,而且它是有序的,能够存储 2 的 32 次方减 1 个节点(超过 40 亿个节点)。

Redis 链表是双向的,因此即可以从左到右,也可以从右到左遍历它存储的节点,链表结构如图 1 所示。

链表结构

​ 图 1 链表结构

由于是双向链表,所以只能够从左到右,或者从右到左地访问和操作链表里面的数据节点。但是使用链表结构就意味着读性能的丧失,所以要在大量数据中找到一个节点的操作性能是不佳的,因为链表只能从一个方向中去遍历所要节点。

比如从查找节点 10 000 开始查询,它需要按照节点 1、节点 2、节点 3……直至节点 10 000,这样的顺序查找,然后把一个个节点和你给出的值比对,才能确定节点所在。如果这个链表很大,如有上百万个节点,可能需要遍历几十万次才能找到所需要的节点,显然查找性能是不佳的。

而链表结构的优势在于插入和删除的便利,因为链表的数据节点是分配在不同的内存区域的,并不连续,只是根据上一个节点保存下一个节点的顺序来索引而已,无需移动元素。其新增和删除的操作如图 2 所示。

链表的新增和删除操作

​ 图 2 链表的新增和删除操作

​ 图 2 的阿拉伯数字代表新增的步骤,而汉字数字代表删除步骤。

  • 新增节点:对插入图中的节点 4 而言,先看从左到右的指向,先让节点 4 指向节点 1 原来的下一个节点,也就是节点 2,然后让节点 1 指向节点 4,这样就完成了从右到左的指向修改。再看从右到左,先让节点 4 指向节点 1,然后节点 2 指向节点 4,这个时候就完成了从右到左的指向,那么节点 1 和节点 2 之间的原有关联关系都已经失效,这样就完成了在链表中新增节点4的功能。
  • 删除节点:对删除图中的节点 3 而言,首先让节点 2 从左到右指向后续节点,然后让后续节点指向节点 2,这样节点 3 就脱离了链表,也就是断绝了与节点 2 和后继节点的关联关系,然后对节点 3 进行内存回收,无须移动任何节点,就完成了删除。

由此可见,链表结构的使用是需要注意场景的,对于那些经常需要对数据进行插入和删除的列表数据使用它是十分方便的,因为它可以在不移动其他节点的情况下完成插入和删除。而对于需要经常查找的,使用它性能并不佳,它只能从左到右或者从右到左的查找和比对。

因为是双向链表结构,所以 Redis 链表命令分为左操作和右操作两种命令,左操作就意味着是从左到右,右操作就意味着是从右到左。

Redis 关于链表的命令如表 1 所示。

命 令 说 明 备 注
lpush key node1 [node2.]..... 把节点 node1 加入到链表最左边 如果是 node1、node2 ...noden 这样加入, 那么链表开头从左到右的顺序是 noden...node2、node1
rpush key node1[node2]...... 把节点 node1 加入到链表的最右边 如果是 node1、node2....noden 这样加 入,那么链表结尾从左到右的顺序是 node1、node2,node3...noden
lindex key index 读取下标为 index 的节点 返回节点字符串,从 0 开始算
llen key 求链表的长度 返回链表节点数
lpop key 删除左边第一个节点,并将其返回 ——
rpop key 删除右边第一个节点,并将其返回 ——
linsert key before|after pivot node 插入一个节点 node,并且可以指定在值为pivot 的节点的前面(before)或者后面(after)) 如果 list 不存在,则报错;如果没有值为对应 pivot 的,也会插入失败返回 -1
lpushx list node 如果存在 key 为 list 的链表,则插入节点 node, 并且作为从左到右的第一个节点 如果 list 不存在,则失败
rpushx list node 如果存在 key 为 list 的链表,则插入节点 node,并且作为从左到右的最后个节点 如果 list 不存在,则失败
lrange list start end 获取链表 list 从 start 下标到 end 下标的节点值 包含 start 和 end 下标的值
lrem list count value 如果 count 为 0,则删除所有值等于 value 的节 点:如果 count 不是 0,则先对 count 取绝对值,假设记为 abs,然后从左到右删除不大于 abs 个等于 value 的节点 注意,count 为整数,如果是负数,则 Redis 会先求取其绝对值,然后传递到后台操作
lset key index node 设置列表下标为 index 的节点的值为 node ——
ltrim key start stop 修剪链表,只保留从 start 到 stop 的区间的节点,其余的都删除掉 包含 start 和 end 的下标的节点会保留

表所列举的就是常用的链表命令,其中以“l”开头的代表左操作,以“r”开头的代表右操作。对于很多个节点同时操作的,需要考虑其花费的时间,链表数据结构对于查找而言并不适合于大数据,而 Redis 也给了比较灵活的命令对其进行操作。Redis 关于链表的操作命令,如图 3 所示。

Redis关于链表的操作命令

​ 图 3 Redis关于链表的操作命令

这里展示了关于 Redis 链表的常用命令,只是对于大量数据操作的时候,我们需要考虑插入和删除内容的大小,因为这将是十分消耗性能的命令,会导致 Redis 服务器的卡顿。对于不允许卡顿的一些服务器,可以进行分批次操作,以避免出现卡顿。

需要指出的是,之前这些操作链表的命令都是进程不安全的,因为当我们操作这些命令的时候,其他 Redis 的客户端也可能操作同一个链表,这样就会造成并发数据安全和一致性的问题,尤其是当你操作一个数据量不小的链表结构时,常常会遇到这样的问题。

为了克服这些问题,Redis 提供了链表的阻塞命令,它们在运行的时候,会给链表加锁,以保证操作链表的命令安全性,如表所示。

命 令 说 明 备 注
blpop key timeout 移出并获取列表的第一个元索,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元索为止 相对于 lpop 命令,它的操作是进程安全的
brpop key timeout 移出并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 相对于 rpop 命令,它的操作是进程安全的
rpoplpush key sre dest 按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表最左边 不能设置超时时间
brpoplpush key src dest timeout 按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表最左边,并可以设置超时时间 可设置超时时间

当使用这些命令时,Redis 就会对对应的链表加锁,加锁的结果就是其他的进程不能再读取或者写入该链表,只能等待命令结束。加锁的好处可以保证在多线程并发环境中数据的一致性,保证一些重要数据的一致性,比如账户的金额、商品的数量。

不过在保证这些的同时也要付出其他线程等待、线程环境切换等代价,这将使得系统的并发能力下降,关于多线程并发锁,未来还会提及,这里先看 Redis 链表阻塞操作命令,如图 4 所示。

Redis链表阻塞操作命令

​ 图 4 Redis 链表阻塞操作命令

在实际的项目中,虽然阻塞可以有效保证了数据的一致性,但是阻塞就意味着其他进程的等待,CPU 需要给其他线程挂起、恢复等操作,更多的时候我们希望的并不是阻塞的处理请求,所以这些命令在实际中使用得并不多,后面还会深入探讨关于高并发锁的问题。

使用 Spring 去操作 Redis 链表的命令,这里继续保持代码清单18-5关于 RedisTemplate 的配置,在此基础上获取 RedisTemplate 对象,然后输入以下代码,它实现的是图 3 所示的命令功能,请读者仔细体会。

public static void testList() {
    ApplicationContext applicationcontext = new ClassPathXmlApplicationContext("applicationContext.xml");
    RedisTemplate redisTemplate = applicationcontext.getBean(RedisTemplate.class);
    try {
        //删除链表,以便我们可以反复测试
        redisTemplate.delete("list");
        //把node3插入链表list
        redisTemplate. opsForList ().leftPush ("list", "node3");
        List<String> nodeList = new ArrayList<String>();
        for (int i = 2; i >= 1; i--){
            nodeList.add("nnode" + i);
        }
        //相当于lpush把多个价值从左插入链表
        redisTemplate.opsForList().leftPushAll("list", nodeList);
        //从右边插入一个节点
        redisTemplate.opsForList().rightPush("list", "node4");
        //获取下标为0的节点
        String nodel = (String) redisTemplate.opsForList() .index("list", 0);
        //获取链表长度
        long size = redisTemplate.opsForList ().size ("listn");
        //从左边弹出一个节点
        String lpop = (String) redisTemplate.opsForList().leftPop("list");
        //从右边弹出一个节点
        String rpop = (String) redisTemplate.opsForList().rightPop("list");
        //注意,需要使用更为底层的命令才能操作linsert命令
        //使用linsert命令在node2前插入一个节点
        redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"),RedisListCommands.Position.BEFORE,"node2".getBytes("utf-8"),"before_node".getBytes("utf-8"));
        //使用linsert命令在node2后插入一个节点
        redisTemplate.getConnectionFactory().getConnection().linsert("list".getBytes("utf-8"),RedisListCommands.Position.AFTER,"node2".getBytes("utf-8"), "after_node".getBytes("utf-8"));
        //判断list是否存在,如果存在则从左边插入head节点
        redisTemplate.opsForList().leftPushlfPresent("list", "head");
        //判断list是否存在,如果存在则从右边插入end节点
        redisTemplate.opsForList().rightPushlfPresent("list", "end");
        //从左到右,或者下标从0到10的节点元素
        List valueList = redisTemplate.opsForList().range("list", 0, 10);
        nodeList.clear();
        for (int i = 1; i <= 3; i++) {
            nodeList.add("node");
        }
        //在链表左边插入三个值为node的节点
        redisTemplate.opsForList().leftPushAll.("list", nodeList);
        //从左到右删除至多三个node节点
        redisTemplate.opsForList().remove("list", 3,"node");
        //给链表下标为0的节点设置新值
        redisTemplate.opsForList().set("list",0, "new_head_value");
    } catch (UnsupportedEncodingException ex) {
        ex.printStackTrace();
    }
    //打印链表数据
    printList(redisTemplate, "list");
}
public static void printList(RedisTemplate redisTemplate, String key) { 
//链表长度
    Long size = redisTemplate.opsForList().size(key);
}

这里所展示的是 RedisTemplate 对于 Redis 链表的操作,其中 left 代表左操作,right 代表右操作。有些命令 Spring 所提供的 RedisTemplate 并不能支持,比如 linsert 命令,这个时候可以使用更为底层的方法去操作,正如代码中的这段:

// 使用linsert命令在node2前插入一个节点
redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"),
RedisListCommands.Position.BEFORE,"node2".getBytes("utf-8"),
"before_node".getBytes("utf-8"));

在多值操作的时候,往往会使用 list 进行封装,比如 leftPushAll 方法,对于很大的 list 的操作需要注意性能,比如 remove 这样的操作,在大的链表中会消耗 Redis 系统很多的性能。

正如之前的探讨一样,Redis 还有对链表进行阻塞操作的命令,这里 Spring 也给出了支持,代码如下所示。

public static void testBList()    {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    RedisTemplate redisTemplate    = applicationContext.getBean(RedisTemplate.class);
    // 清空数据,可以重复测试
    redisTemplate.delete ("list1");
    redisTemplate.delete ("list2");
    //初始化链表 list1
    List<String> nodeList = new ArrayList<String>();
    for (int i=1; i<=5; i++)    {
        nodeList.add("node" + i);
    }
    redisTemplate.opsForList().leftPushAll("list1", nodeList);
    // Spring 使用参数超时时间作为阻塞命令区分,等价于 blpop 命令,并且可以设置时间参数 redisTemplate.opsForList().leftPop ("list1", 1, TimeUnit.SECONDS);
    // Spring 使用参数超时时间作为阻塞命令区分,等价于 brpop 命令,并且可以设置时间参数
    redisTemplate.opsForList().rightPop("list1", 1, TimeUnit.SECONDS);
    nodeList.clear();
    // 初始化链表 list2
    for (int i=1; i<=3; i++)    {
        nodeList.add("dato" + i);
    }
    redisTemplate.opsForList().leftPushAll("list2", nodeList);
    // 相当于 rpoplpush 命令,弹出 list1 最右边的节点,插入到 list2 最左边
    redisTemplate.opsForList().rightPopAndLeftPush("list1","list2");
    // 相当于brpoplpush命令,注意在 Spring 中使用超时参数区分 redisTemplate.opsForList().rightPopAndLeftPush("list1", "list2",1,TimeUnit.SECONDS);
    // 打印链表数据
    printList(redisTemplate, "list1");
    printList(redisTemplate, "list2");
}

这里展示了 Redis 关于链表的阻塞命令,在 Spring 中它和非阻塞命令的方法是一致的,只是它会通过超时参数进行区分,而且我们还可以通过方法设置时间的单位,使用还是相当简单的。注意,它是阻塞的命令,在多线程的环境中,它能在一定程度上保证数据的一致而性能却不佳。

Set 数据结构和常用命令

Redis 的集合(set)不是一个线性结构,而是一个哈希表结构,它的内部会根据 hash 分子来存储和查找数据,理论上一个集合可以存储 2 的 32 次方减 1 个节点(大约 42 亿)个元素,因为采用哈希表结构,所以对于 Redis 集合的插入、删除和查找的复杂度都是 0(1),只是我们需要注意 3 点。

  • 对于集合而言,它的每一个元素都是不能重复的,当插入相同记录的时候都会失败。
  • 集合是无序的。
  • 集合的每一个元素都是 String 数据结构类型。

Redis 的集合可以对于不同的集合进行操作,比如求出两个或者以上集合的交集、差集和并集等。集合命令,如表 所示。

命 令 说 明 备 注
sadd key member1 [member2 member3......] 给键为 key 的集合増加成员 可以同时増加多个
scard key 统计键为 key 的集合成员数
sdiffkey1 [key2] 找出两个集合的差集 参数如果是单key,那么 Redis 就返回这个 key 的所有元素
sdiftstore des key1 [key2] 先按 sdiff 命令的规则,找出 key1 和 key2 两 个集合的差集,然后将其保存到 des 集合中。
sinter key1 [key2] 求 key1 和 key2 两个集合的交集。 参数如果是单 key,那么 Redis 就返冋这个 key 的所有元素
sinterstore des key1 key2 先按 sinter 命令的规则,找出 key1 和 key2 两个集合的交集,然后保存到 des 中
sismember key member 判断 member 是否键为 key 的集合的成员 如果是返回 1,否则返回 0
smembers key 返回集合所有成员 如果数据量大,需要考虑迭代遍历的问题
smove src des member 将成员 member 从集合 src 迁移到集合 des 中
spop key 随机弹出集合的一个元素 注意其随机性,因为集合是无序的
srandmember key [count] 随机返回集合中一个或者多个元素,count 为限制返回总数,如果 count 为负数,则先求其绝对值 count 为整数,如果不填默认为 1,如果 count 大于等于集合总数,则返回整个集合
srem key member1[member2......] 移除集合中的元素,可以是多个元素 对于很大的集合可以通过它删除部分元素,避免删除大量数据引发 Redis 停顿
sunion key1 [key2] 求两个集合的并集 参数如果是单 key,那么 Redis 就返回这个 key 的所有元素
sunionstore des key1 key2 先执行 sunion 命令求出并集,然后保存到键为 des 的集合中

表 中命令的前缀都包含了一个 s,用来表达这是集合的命令,集合是无序的,并且支持并集、交集和差集的运算,下面通过命令行客户端来演示这些命令,如图 1 所示。

通过命令行客户端演示这些命令

​ 图 1 通过命令行客户端演示这些命令

交集、并集和差集保存命令的用法,如下图 2 所示。

交集、并集和差集保存命令的用法

​ 图 2 交集、并集和差集保存命令的用法

这里的命令主要是求差集、并集和交集,并保存到新的集合中。至此就展示了表 1 中的所有命令,下面将在 Spring 中操作它们,代码如下所示。

// 请把 RedisTemplate 值序列化器设置为 StringRedisSerializer 测试该代码片段
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
Set set = null;
// 将元素加入列表
redisTemplate.boundSetOps ("set1").add ("vl","v2","v3","v4","v5", "v6");
redisTemplate.boundSetOps ("set2").add( "v0","v2","v4","v6","v8");
//求集合长度
redisTemplate.opsForSet().size ("set1");
//求差集
set = redisTemplate.opsForSet().difference ("set1","set2");
//求并集
set = redisTemplate.opsForSet().intersect ("set1","set2");
//判断是否集合中的元素
boolean exists = redisTemplate.opsForSet().isMember("set1", "vl");
//获取集合所有元素
set = redisTemplate.opsForSet().members ("set1");
//从集合中随机弹出一个元素
String val = (String)redisTemplate.opsForSet().pop("set1");
//随机获取一个集合的元素
val = (String) redisTemplate.opsForSet().randomMember("set1");
//随机获取2个集合的元素
List list = redisTemplate.opsForSet ().randomMembers ("set1", 2L);
//删除一个集合的元素,参数可以是多个
redisTemplate.opsForSet().remove ("setl","v1");
//求两个集合的并集
redisTemplate.opsForSet().union ("set1","set2");
//求两个集合的差集,并保存到集合diff_set中
redisTemplate.opsForSet().differenceAndStore("set1", "set2", "diff_set");
//求两个集合的交集,并保存到集合inter_set中
redisTemplate.opsForSet().intersectAndStore("set1","set2" "inter_set");
//求两个集合的并集,并保存到集合union_set中
redisTemplate.opsForSet().unionAndStore ("set1", "set2", "union_set");

上面的注释已经较为详细地描述了代码的含义,这样我们就可以在实践中使用 Spring 操作 Redis 的集合了。

Zset 数据结构和常用命令

有序集合(zset)和集合(set)类似,只是说它是有序的,和无序集合的主要区别在于每一个元素除了值之外,它还会多一个分数。分数是一个浮点数,在 Java 中是使用双精度表示的,根据分数,Redis 就可以支持对分数从小到大或者从大到小的排序。

这里和无序集合一样,对于每一个元素都是唯一的,但是对于不同元素而言,它的分数可以一样。元素也是 String 数据类型,也是一种基于 hash 的存储结构。

集合是通过哈希表实现的,所以添加、删除、查找的复杂度都是 0(1)。集合中最大的成员数为 2 的 32 次方减 1(40 多亿个成员),有序集合的数据结构如图 1 所示。

有序集合的数据结构

​ 图 1 有序集合的数据结构

有序集合是依赖 key 标示它是属于哪个集合,依赖分数进行排序,所以值和分数是必须的,而实际上不仅可以对分数进行排序,在满足一定的条件下,也可以对值进行排序。

有序集合和无序集合的命令是接近的,只是在这些命令的基础上,会增加对于排序的操作,这些是我们在使用的时候需要注意的细节。

有些时候 Redis 借助数据区间的表示方法来表示包含或者不包含,比如在数学的区间表示中,[2,5] 表示包含 2,但是不包含 5 的区间。具体如表所示。

命 令 说 明 备 注
zadd key score1 value1 [score2 value2......] 向有序集合的 key,增加一个或者多个成员 如果不存在对应的 key,则创建键为 key 的有序集合
zcard key 获取有序集合的成员数
zcount key min max 根据分数返回对应的成员列表 min 为最小值,max 为最大值,默认为包含 min 和 max 值,采用数学区间表示的方法,如果需要不包含,则在分数前面加入“(”,注意不支持“[”表示
zincrby key increment member 给有序集合成员值为 member 的分数增加 increment
zinterstore desKey numkeys key1 [key2 key3......] 求多个有序集合的交集,并且将结果保存到 desKey 中 numkeys 是一个整数,表示多少个有序集合
zlexcount key min max 求有序集合 key 成员值在 min 和 max 的范围 这里范围为 key 的成员值,Redis 借助数据区间的表示方法,“[”表示包含该值,“(”表示不包含该值
zrange key start stop [withscores] 按照分值的大小(从小到大)返回成员,加入 start 和 stop 参数可以截取某一段返回。如果输入可选项 withscores,则连同分数一起返回 这里记集合最人长度为 len,则 Redis 会将集合排序后,形成一个从 0 到 len-1 的下标,然后根据 start 和 stop 控制的下标(包含 start 和 stop)返回
zrank key member 按从小到大求有序集合的排行 排名第一的为 0,第二的为 1……
zrangebylex key min max [limit offset count] 根据值的大小,从小到大排序,min 为最小值,max 为最大值;limit 选项可选,当 Redis 求出范围集合后,会生产下标 0 到 n,然后根据偏移量 offset 和限定返回数 count,返回对应的成员 这里范围为 key 的成员值,Redis 借助数学区间的表示方法,“[”表示包含该值,“(”表示不包含该值
zrangebyscore key min max [withscores] [limit offset count] 根据分数大小,从小到大求取范围,选项 withscores 和 limit 请参考 zrange 命令和 zrangebylex 说明 根据分析求取集合的范围。这里默认包含 min 和 max,如果不想包含,则在参数前加入“(”, 注意不支持“[”表示
zremrangebyscore key start stop 根据分数区间进行删除 按照 socre 进行排序,然后排除 0 到 len-1 的下标,然后根据 start 和 stop 进行删除,Redis 借助数学区间的表示方法,“[”表示包含该值,“(” 表示不包含该值
zremrangebyrank key start stop 按照分数排行从小到大的排序删除,从 0 开始计算
zremrangebylex key min max 按照值的分布进行删除
zrevrange key start stop [withscores] 从大到小的按分数排序,参数请参见 zrange 与 zrange 相同,只是排序是从大到小
zrevrangebyscore key max min [withscores] 从大到小的按分数排序,参数请参见 zrangebyscore 与 zrangebyscore 相同,只是排序是从大到小
zrevrank key member 按从大到小的顺序,求元素的排行 排名第一位 0,第二位 1......
zscore key member 返回成员的分数值 返回成员的分数
zunionstore desKey numKeys key1 [key2 key3 key4......] 求多个有序集合的并集,其中 numKeys 是有序集合的个数 ——

在对有序集合、下标、区间的表示方法进行操作的时候,需要十分小心命令,注意它是操作分数还是值,稍有不慎就会出现问题。

这里命令比较多,也有些命令比较难使用,在使用的时候,务必要小心,不过好在我们使用 zset 的频率并不是太高,下面是测试结果——有序集合命令展示,如图所示。

有序集合命令展示

​ 图 2 有序集合命令展示

出演示的例子。测试代码如下所示。

public static void testZset() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                "applicationContext.xml");
    RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
    // Spring提供接口 TypedTuple操作有序集合
    Set<TypedTuple> set1 = new HashSet<TypedTuple>();
    Set<TypedTuple> set2 = new HashSet<TypedTuple>();
    int j = 9;
    for (int i = 1; i <= 9; i++) {
        j--;
        // 计算分数和值
        Double score1 = Double.valueOf(i);
        String value1 = "x" + i;
        Double score2 = Double.valueOf(j);
        String value2 = j % 2 == 1 ? "y" + j : "x" + j;
        // 使用 Spring 提供的默认 TypedTuple--DefaultTypedTuple
        TypedTuple typedTuple1 = new DefaultTypedTuple(value1, score1);
        set1.add(typedTuple1);
        TypedTuple typedTuple2 = new DefaultTypedTuple(value2, score2);
        set2.add(typedTuple2);
    }
    // 将元素插入有序集合zset1
    redisTemplate.opsForZSet().add("zset1", set1);
    redisTemplate.opsForZSet().add("zset2", set2);
    // 统计总数
    Long size = null;
    size = redisTemplate.opsForZSet().zCard("set1");
    // 计分数为score,那么下面的方法就是求 3<=score<=6的元素
    size = redisTemplate.opsForZSet().count("zset1", 3, 6);
    Set set = null;
    // 从下标一开始截取5个元素,但是不返回分数,每一个元索是String
    set = redisTemplate.opsForZSet().range("zset1", 1, 5);
    printSet(set);
    // 截取集合所有元素,并且对集合按分数排序,并返回分数,每一个元素是TypedTuple
    set = redisTemplate.opsForZSet().rangeWithScores("zset1", 0, -1);
    printTypedTuple(set);
    // 将zset1和zset2两个集合的交集放入集合inter_zset
    size = redisTemplate.opsForZSet().intersectAndStore("zset1", "zset2","inter_zset");
    // 区间
    Range range = Range.range();
    range.lt("x8");// 小于
    range.gt("x1"); // 大于
    set = redisTemplate.opsForZSet().rangeByLex("zset1", range);
    printSet(set);
    range.lte("x8"); // 小于等于
    range.gte("xl"); // 大于等于
    set = redisTemplate.opsForZSet().rangeByLex("zset1", range);
    printSet(set);
    // 限制返回个数
    Limit limit = Limit.limit();
    // 限制返回个数
    limit.count(4);
    // 限制从第五个开始截取
    limit.offset(5);
    // 求区间内的元素,并限制返回4条
    set = redisTemplate.opsForZSet().rangeByLex("zset1", range, limit);
    printSet(set);
    // 求排行,排名第1返回0,第2返回1
    Long rank = redisTemplate.opsForZSet().rank("zset1", "x4");
    System.err.println("rank = " + rank);
    // 删除元素,返回删除个数
    size = redisTemplate.opsForZSet().remove("zset1", "x5", "x6");
    System.err.println("delete = " + size);
    // 按照排行删除从0开始算起,这里将删除第排名第2和第3的元素
    size = redisTemplate.opsForZSet().removeRange("zset2", 1, 2);
    // 获取所有集合的元素和分数,以-1代表全部元素
    set = redisTemplate.opsForZSet().rangeWithScores("zset2", 0, -1);
    printTypedTuple(set);
    // 删除指定的元素
    size = redisTemplate.opsForZSet().remove("zset2", "y5", "y3");
    System.err.println(size);
    // 给集合中的一个元素的分数加上11
    Double dbl = redisTemplate.opsForZSet().incrementScore("zset1", "x1",11);
    redisTemplate.opsForZSet().removeRangeByScore("zset1", 1, 2);
    set = redisTemplate.opsForZSet().reverseRangeWithScores("zset2", 1, 10);
    printTypedTuple(set);
}

/**
* 打印TypedTuple集合
* @param set
* -- Set<TypedTuple>
*/
public static void printTypedTuple(Set<TypedTuple> set) {
    if (set != null && set.isEmpty()) {
        return;
    }
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        TypedTuple val = (TypedTuple) iterator.next();
        System.err.print("{value = " + val.getValue() + ", score = "
                + val.getScore() + "}\n");
    }
}

/**
* 打印普通集合
* @param set普通集合
*/
public static void printSet(Set set) {
    if (set != null && set.isEmpty()) {
        return;
    }
    Iterator iterator = set.iterator();
    while (iterator .hasNext()) {
    Object val = iterator.next();
    System. out.print (val +"\t");
    }
    System.out.println();
}

Redis HyperLogLog常用命令

基数是一种算法。举个例子,一本英文著作由数百万个单词组成,你的内存却不足以存储它们。

英文单词本身是有限的,在这本书的几百万个单词中有许许多多重复单词,扣去重复的单词,这本书中也就是几千到一万多个单词而已,那么内存就足够存储它。

比如数字集合 {1,2,5,7,9,1,5,9} 的基数集合为 {1,2,5,7,9} 那么基数(不重复元素)就是 5,基数的作用是评估大约需要准备多少个存储单元去存储数据,但是基数的算法一般会存在一定的误差(一般是可控的)。Redis 对基数数据结构的支持是从版本 2.8.9 开始的。

基数并不是存储元素,存储元素消耗内存空间比较大,而是给某一个有重复元素的数据集合(一般是很大的数据集合)评估需要的空间单元数,所以它没有办法进行存储,加上在工作中用得不多,所以简要介绍一下 Redis 的 HyperLogLog 命令就可以了,如表所示。

命 令 说 明 备 注
pfadd key element 添加指定元素到 HyperLogLog 中 如果已经存储元索,则返回为 0,添加失败
pfcount key 返回 HyperLogLog 的基数值
pfmerge desKey key1 [key2 key3...] 合并多个 HyperLogLog,并将其保存在 desKey 中

在命令行中演示一下它们,如图 1 所示。

Redis的HyperLogLog命令演示

图 1 Redis 的 HyperLogLog 命令演示

分析一下逻辑,首先往一个键为 h1 的 HyperLogLog 插入元素,让其计算基数,到了第 5 个命令“pfadd h1 a”的时候,由于在此以前已经添加过,所以返回了 0。它的基数集合是 {a,b,c,d},故而求集合长度为 4;之后再添加了第二个基数,它的基数集合是{a,z},所以在 h1 和 h2 合并为 h3 的时候,它的基数集合为 {a,b,c,d,z},所以求取它的基数就是 5。

在 Spring 中操作基数,代码如下所示。

ApplicationContext applicationContext = new ClassPathXmlApplicationcontext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
redisTemplate.opsForHyperLogLog().add("HyperLogLog", "a", "b" , "c", "d", "a");
redisTemplate.opsForHyperLogLog().add("HyperLogLog2", "a"); redisTemplate.opsForHyperLogLog().add("HyperLogLog2", "z");
Long size = redisTemplate.opsForHyperLogLog().size("HyperLogLog");
System.err.println(size);
size = redisTemplate.opsForHyperLogLog().size("HyperLogLog2");
System.err.println(size);
redisTemplate.opsForHyperLogLog().union ("des_key","HyperLogLog","HyperLogLog2");
size = redisTemplate.opsForHyperLogLog().size("des_key");
System.err.println(size);

从上面的代码可以看到,增加一个元素到基数中采用 add 方法,它可以是一个或者多个元素,而求基数大小则是采用了 size 方法,合并基数则采用了 union 方法,其第一个是目标基数的 key,然后可以是一到多个 key。

posted @ 2020-09-03 09:16  夜雨流云  阅读(125)  评论(0编辑  收藏  举报