Redis-07Redis数据结构--有序集合ZSet

简介: Redis-07Redis数据结构--有序集合ZSet

概述


有序集合和集合类似,只是说它是有序的,和无序集合的主要区别在于每一个元素除了值之外,它还会多一个分数。


分数是一个浮点数,在 Java 中是使用双精度表示的,根据分数, Redis 就可以支持对分数从小到大或者从大到小的排序

和无序集合一样,对于每一个元素都是唯一的 ,但是对于不同元素而言,它的分数可以一样

元素也是 String 数据类型,也是一种基于 hash 的存储结构。

集合是通过哈希表实现的,所以添加、删除、 查找的复杂度都是 0(1)

集合中最大的成员数为 2的32次方减 1 ( 40 多亿个成员)


有序集合的数据结构


20180926170921520.png


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


Redis 有序集合的部分命令


官网: https://redis.io/commands#sorted_set


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


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


20190109233258647.png


image.png

image.png

image.png


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

# 为了测试的数据干净,删除当前db的数据
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> 
127.0.0.1:6379> 
#  zadd key score1 value1 [score2 value2 …] 向有序集合zset1 ,增加9个成员 
127.0.0.1:6379> ZADD zset1 1 x1 2 x2 3 x3 4 x4 5 x5 6 x6 7 x7 8 x8 9 x9
(integer) 9
#  zadd key score1 value1 [score2 value2 …] 向有序集合zset2 ,增加9个成员
127.0.0.1:6379> ZADD zset2 1 y1 2 x2 3 y3 4 x4 5 y5 6 x6 7 y7 8 x8 9 y9
(integer) 9
# zcard key 获取有序集合zset1的成员数
127.0.0.1:6379> ZCARD zset1
(integer) 9
# zcount key min max  根据分数返回对应的成员列表
127.0.0.1:6379> ZCOUNT zset1 1 4 
(integer) 4
# zinterstore desKey nurnkeys key1 [key2 key3 …] 求多个有序集合的交集,并且将结果保存到 des Key 中
127.0.0.1:6379> ZINTERSTORE des_key 2 zset1 zset2 
(integer) 4
# zlexcount key min max  求有序集合 zset1 成员值在 min 和 max 的范围 [ 表示包含该值,( 表示不包含该值
127.0.0.1:6379> ZLEXCOUNT zset1 (x1 [x5
(integer) 4
# zrange key start stop [withscores] 按照分值的大小(从小到大)返回成员,加入 start 和 stop 参数可以截取某一段返回.如果输入可选项 withscores,则连同分数一起返回
127.0.0.1:6379> ZRANGE zset1 1 5 withscores
 1) "x2"
 2) "2"
 3) "x3"
 4) "3"
 5) "x4"
 6) "4"
 7) "x5"
 8) "5"
 9) "x6"
10) "6"
#  zrank key member 按从小到大求有序集合的排行
127.0.0.1:6379> ZRANK zset1 x5
(integer) 4
#  zrangebylex key min max [limit offset count]根据值的大小,从小到大排序   [表示包含该值 (表示不包含该值
127.0.0.1:6379> ZRANGEBYLEX zset1 (x1 [x6
1) "x2"
2) "x3"
3) "x4"
4) "x5"
5) "x6"
127.0.0.1:6379> 
# zrangebyscore key min max [withscores] [limit offset count] 根据分数大小,从小到大求取范围
127.0.0.1:6379> ZRANGEBYSCORE zset1 5 7 
1) "x5"
2) "x6"
3) "x7"
# zrangebyscore key min max [withscores] [limit offset count] 根据分数大小,从小到大求取范围
127.0.0.1:6379> ZRANGEBYSCORE zset1 2 7 withscores limit 1 5 
 1) "x3"
 2) "3"
 3) "x4"
 4) "4"
 5) "x5"
 6) "5"
 7) "x6"
 8) "6"
 9) "x7"
10) "7"
# zrevrange key start stop [withscores] 从大到小的按分数排序
127.0.0.1:6379> ZREVRANGE zset1 1 5 
1) "x8"
2) "x7"
3) "x6"
4) "x5"
5) "x4"
#  zrevrangebyscore key max min [withscores] 从大到小的按分数排序
127.0.0.1:6379> ZREVRANGEBYSCORE zset2 5 2 withscores
1) "y5"
2) "5"
3) "x4"
4) "4"
5) "y3"
6) "3"
7) "x2"
8) "2"
#  zrevrank key member 按从大到小的顺序,求元素的排行
127.0.0.1:6379> ZREVRANK zset1 x4
(integer) 5
# zscore key member 返回成员的分数值
127.0.0.1:6379> ZSCORE zset1 x5
"5"
# zunionstore desKey numKeys key1 [key2 key3 key4 …] 求多个有序集合的并集,其中 numKeys是有序,集合的个数
127.0.0.1:6379> ZUNIONSTORE des_key 2 zset1 zset2
(integer) 14
# zincrby key increment member 给有序集合成员值为 member 的分数增加 increment
127.0.0.1:6379> ZINCRBY zset1 5 x9
"14"
# zremrangebyscore key start stop 根据分数区间进行删除
127.0.0.1:6379> ZREMRANGEBYSCORE zset1 3 2
(integer) 0
# zremrangebyscore key start stop 根据分数区间进行删除
127.0.0.1:6379> ZREMRANGEBYSCORE zset1 3 5 
(integer) 3
# zremrangebyrank key start stop 按照分数排行从小到大的排序删除,从0开始计算
127.0.0.1:6379> ZREMRANGEBYRANK zset1 1 3 
(integer) 3
# zremrangebylex key min max  按照值的分布进行删除
127.0.0.1:6379> ZREMRANGEBYLEX zset2 [y1 [y5
(integer) 6
127.0.0.1:6379>
127.0.0.1:6379> ZCARD zset1
(integer) 3
127.0.0.1:6379> ZCARD zset2
(integer) 3
127.0.0.1:6379> 
127.0.0.1:6379> ZRANGE zset1 0 999
1) "x1"
2) "x8"
3) "x9"
127.0.0.1:6379> ZRANGE zset2  0 999
1) "y7"
2) "x8"
3) "y9"
127.0.0.1:6379> 


spring-data-redis 对有序集合的封装


在 Spring 中使用 Redis 的有序集合,需要注意的是 Spring 对 Redis 有序集合的元素的值和分数的范围( Range )和限制( Limit)进行了封装。


我们来看下Spring是如何封装的。 先介绍一个主要的接口一一TypedTuple,它不是一个普通的接口,而一个内部接口.


org.springframework . data. redis.core .ZSetOperations 接口的内部接口,它定义了两个方

2018092620370335.png


  • getValue()是获取值, getScore()是获取分数,但是它只是一个接口,而不是一个实现类
  • spring-data-red is 提供了 一个默认的实现类一DefaultTypedTuple


20180926203841154.png


在默认的情况下 Spring 就会把带有分数的有序集合的值和分数封装到这个类中 ,这样就可以通过这个类对象读取对应的值和分数了 .


Spring 不仅对有序集合元素封装,而且对范围也进行了封装,方便使用.它是使用接口 org.springframe.work.data.redis.connection.RedisZSetCommands 下的内部类 Range 进行封装的,它有一个静态的 range()方法,使用它就可以生成一个 Range 对象了,只是要清楚 Range对象的几个方法才行.


20180926204117884.png

//  设置大于等于 min
public Range gte(Object min)
//  设置大于 min
public Range gt(Object min)
//  设置小于等于 max
public Range lte(Object max)
//  设置小于 max
public Range lt(Object max)


这 4 个方法就是最常用的范围方法.

下面看一下limit,它是接口 org.springframework.data.redis.connection.RedisZSetCommands 下的内部类,它是一个简单的 POJO,它存在两个属性


20180926204526950.png


通过属性的名称很容易知道:offset 代表从第几个开始截取,而 count 代表限制返回的总数量。


使用 Spring 操作有序集合


刚才讨论了 spring-data-redis 项目对有序集合的封装,在此基础上这里的演示示例代码在测试代码前,要把 RedisTemplate 的 keySerializer 和 valueSerializer属性都修改为字符串序列化器 StringRedisSerializer


配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context.xsd">
    <context:property-placeholder location="classpath:redis/redis.properties" />
    <!--2,注意新版本2.3以后,JedisPoolConfig的property name,不是maxActive而是maxTotal,而且没有maxWait属性,建议看一下Jedis源码或百度。 -->
    <!-- redis连接池配置 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!--最大空闲数 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
        <!--连接池的最大数据库连接数 -->
        <property name="maxTotal" value="${redis.maxTotal}" />
        <!--最大建立连接等待时间 -->
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        <!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟) -->
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        <!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 -->
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        <!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 -->
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
      <property name="testOnBorrow" value="true"></property>
    <property name="testOnReturn" value="true"></property>
    <property name="testWhileIdle" value="true"></property>
    </bean>
  <!--redis连接工厂 -->
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="poolConfig" ref="jedisPoolConfig"></property>
        <!--IP地址 -->
        <property name="hostName" value="${redis.host.ip}"></property>
        <!--端口号 -->
        <property name="port" value="${redis.port}"></property>
        <!--如果Redis设置有密码 -->
        <property name="password" value="${redis.password}" /> 
        <!--客户端超时时间单位是毫秒 -->
        <property name="timeout" value="${redis.timeout}"></property>
        <property name="usePool" value="true" />
        <!--<property name="database" value="0" /> -->
    </bean>
  <!-- 键值序列化器设置为String 类型 -->
  <bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
  <!-- redis template definition -->
  <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
    p:connection-factory-ref="jedisConnectionFactory"
    p:keySerializer-ref="stringRedisSerializer"
    p:defaultSerializer-ref="stringRedisSerializer"
    p:valueSerializer-ref="stringRedisSerializer">
  </bean>
</beans>
package com.artisan.redis.baseStructure.zset;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.connection.RedisZSetCommands.Limit;
import org.springframework.data.redis.connection.RedisZSetCommands.Range;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
public class SpringRedisZSetDemo {
  private static final String ZSET1 = "zset1";
  private static final String ZSET2 = "zset2";
  @SuppressWarnings({ "unchecked", "rawtypes" })
  public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-zset.xml");
    RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate");
    // 方便测试,清空数据
    redisTemplate.delete(ZSET1);
    redisTemplate.delete(ZSET2);
    // Spring 提供接口 TypedTuple 操作有序集合
    Set<TypedTuple> set1 = new HashSet<ZSetOperations.TypedTuple>();
    Set<TypedTuple> set2 = new HashSet<ZSetOperations.TypedTuple>();
    // 构造数据
    // 127.0.0.1:6379>
    // # zadd key score1 value1 [score2 value2 …] 向有序集合zset1 ,增加9个成员
    // >ZADD zset1 1 x1 2 x2 3 x3 4 x4 5 x5 6 x6 7 x7 8 x8 9 x9
    // (integer) 9
    // # zadd key score1 value1 [score2 value2 …] 向有序集合zset2 ,增加9个成员
    // > ZADD zset2 1 y1 2 x2 3 y3 4 x4 5 y5 6 x6 7 y7 8 x8 9 y9
    // (integer) 9
    int j = 9;
    String value1, value2 = null;
    double score1, score2 = 0.0;
    for (int i = 1; i <= 9; i++) {
      // 计算分数和值
       score1 = Double.valueOf(i);
       value1 = "x" + i;
      if (j > 0) {
        score2 = Double.valueOf(j);
        value2 = j % 2 == 1 ? "y" + j : "x" + j;
        j--;
      }
      // 使用 Spring提供的默认 TypedTuple-DefaultTypedTuple
      TypedTuple typedTuplel = new DefaultTypedTuple(value1,score1);
      set1.add(typedTuplel);
      TypedTuple typedTuple2 = new DefaultTypedTuple(value2,score2);
      set2.add(typedTuple2);
    }
    // 写入redis
    redisTemplate.opsForZSet().add(ZSET1, set1);
    redisTemplate.opsForZSet().add(ZSET2, set2);
    // 统计总数
    Long size = redisTemplate.opsForZSet().size(ZSET1);
    System.out.println(ZSET1 + "的size为" + size);
    // 计分数为 score ,那么下面的方法就是求 3<=score<=6 的元素
    Long count = redisTemplate.opsForZSet().count(ZSET1, 3, 6);
    System.out.println(ZSET1 + "中3<=score<=6 的count为" + count);
    // 从下标一开始截驭 5 个元素,但是不返回分数 , 每一个元素是 String
    Set set = redisTemplate.opsForZSet().range(ZSET1, 1, 5);
    printSet(set);
    // 截取集合所有元素,并且对集合按分数排序,并返回分数 , 每一个元素是 TypedTuple
    Set<TypedTuple> typedTuples = redisTemplate.opsForZSet().rangeWithScores(ZSET1, 0, -1);
    printTypedTuple(typedTuples);
    // 将 zsetl 和 zset2 两个集合的交集放入集合 inter_zset
    size = redisTemplate.opsForZSet().intersectAndStore(ZSET1, ZSET2, "inter_zset");
    System.out.println("inter_zset size:" + size);
    // 查看交集inter_zset中的数据
    set = redisTemplate.opsForZSet().range("inter_zset", 0, redisTemplate.opsForZSet().size("inter_zset"));
    printSet(set);
    // 区间
    Range range = Range.range();
    range.lt("x8");// 小于
    range.gt("x1");// 大于
    set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range);
    printSet(set);
    range.lte("x8");// 小于等于
    range.gte("x1");// 大于等于
    set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range);
    printSet(set);
    // 限制返回个数
    Limit limit = Limit.limit();
    // 限制返回个数
    limit.count(4);
    // 限制从第2个开始截取
    limit.offset(2);
    // 求区间内的元素,并限制返回 4 条
    set = redisTemplate.opsForZSet().rangeByLex(ZSET1, range, limit);
    printSet(set);
    // 求排行,排名第 1 返回 0 ,第 2 返回 1
    Long rank = redisTemplate.opsForZSet().rank(ZSET1, "x4");
    System.out.println("rank=" + rank);
    // 删除元素 , 返回删除个数
    size = redisTemplate.opsForZSet().remove(ZSET1, "x5", "x6");
    System.out.println("remove " + size + " 个元素");
    // 按照排行删除从 0 开始算起,这里将删除第排名第 2 和第 3 的元素
    size = redisTemplate.opsForZSet().removeRange(ZSET1, 1, 2);
    System.out.println("removeRange " + size + " 个元素");
    // 获取所有集合的元索和分数 , 以 -1 代表全部元素
    typedTuples = redisTemplate.opsForZSet().rangeWithScores(ZSET1, 0, -1);
    printTypedTuple(typedTuples);
    // 删除指定的元素
    size = redisTemplate.opsForZSet().remove(ZSET2, "y3", "y5");
    System.out.println("remove " + size + " 个元素");
    // 给集合中的一个元素的分数加上 11
    Double double1 = redisTemplate.opsForZSet().incrementScore(ZSET2, "y1", 11);
    printTypedTuple(redisTemplate.opsForZSet().rangeWithScores(ZSET2, 0, redisTemplate.opsForZSet().size(ZSET2)));
    // 从大到小排列
    typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(ZSET2, 0, 99);
    printTypedTuple(typedTuples);
  }
  @SuppressWarnings("rawtypes")
  public static void printTypedTuple(Set<TypedTuple> typedTuples) {
    if (typedTuples != null && typedTuples.isEmpty()) {
      return;
    }
    Iterator<TypedTuple> iterator = typedTuples.iterator();
    while (iterator.hasNext()) {
      TypedTuple typedTuple = iterator.next();
      System.out.println("{value =" + typedTuple.getValue() + ", score=" + typedTuple.getScore() + "}");
    }
    System.out.println("----------------------");
  }
  @SuppressWarnings("rawtypes")
  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.println(val + "\t");
    }
    System.out.println("----------------------");
  }
}

输出

INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Wed Sep 26 23:26:54 CST 2018]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-zset.xml]
zset1的size为9
zset1中3<=score<=6 的count为4
x2  
x3  
x4  
x5  
x6  
----------------------
{value =x1, score=1.0}
{value =x2, score=2.0}
{value =x3, score=3.0}
{value =x4, score=4.0}
{value =x5, score=5.0}
{value =x6, score=6.0}
{value =x7, score=7.0}
{value =x8, score=8.0}
{value =x9, score=9.0}
----------------------
inter_zset size:4
x2  
x4  
x6  
x8  
----------------------
x2  
x3  
x4  
x5  
x6  
x7  
----------------------
x1  
x2  
x3  
x4  
x5  
x6  
x7  
x8  
----------------------
x3  
x4  
x5  
x6  
----------------------
rank=3
remove 2 个元素
removeRange 2 个元素
{value =x1, score=1.0}
{value =x4, score=4.0}
{value =x7, score=7.0}
{value =x8, score=8.0}
{value =x9, score=9.0}
----------------------
remove 2 个元素
{value =x2, score=2.0}
{value =x4, score=4.0}
{value =x6, score=6.0}
{value =y7, score=7.0}
{value =x8, score=8.0}
{value =y9, score=9.0}
{value =y1, score=12.0}
----------------------
{value =y1, score=12.0}
{value =y9, score=9.0}
{value =x8, score=8.0}
{value =y7, score=7.0}
{value =x6, score=6.0}
{value =x4, score=4.0}
{value =x2, score=2.0}
----------------------


注意


使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。


代码


代码托管到了 https://github.com/yangshangwei/redis_learn

相关文章
|
4月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
328 6
|
3月前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
309 86
|
5月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
3月前
|
存储 消息中间件 NoSQL
Redis数据结构:别小看这5把“瑞士军刀”,用好了性能飙升!
Redis提供5种基础数据结构及多种高级结构,如String、Hash、List、Set、ZSet,底层通过SDS、跳表等实现高效操作。灵活运用可解决缓存、计数、消息队列、排行榜等问题,结合Bitmap、HyperLogLog、GEO更可应对签到、UV统计、地理位置等场景,是高性能应用的核心利器。
|
3月前
|
存储 缓存 NoSQL
Redis基础命令与数据结构概览
Redis是一个功能强大的键值存储系统,提供了丰富的数据结构以及相应的操作命令来满足现代应用程序对于高速读写和灵活数据处理的需求。通过掌握这些基础命令,开发者能够高效地对Redis进行操作,实现数据存储和管理的高性能方案。
125 12
|
3月前
|
存储 消息中间件 NoSQL
【Redis】常用数据结构之List篇:从常用命令到典型使用场景
本文将系统探讨 Redis List 的核心特性、完整命令体系、底层存储实现以及典型实践场景,为读者构建从理论到应用的完整认知框架,助力开发者在实际业务中高效运用这一数据结构解决问题。
|
3月前
|
存储 缓存 NoSQL
【Redis】 常用数据结构之String篇:从SET/GET到INCR的超全教程
无论是需要快速缓存用户信息,还是实现高并发场景下的精准计数,深入理解String的特性与最佳实践,都是提升Redis使用效率的关键。接下来,让我们从基础命令开始,逐步揭开String数据结构的神秘面纱。
|
7月前
|
存储 NoSQL 算法
Redis设计与实现——数据结构与对象
Redis 是一个高性能的键值存储系统,其数据结构设计精妙且高效。主要包括以下几种核心数据结构:SDS、链表、字典、跳跃表、整数集合、压缩列表。此外,Redis 对象通过类型和编码方式动态转换,优化内存使用,并支持引用计数、共享对象和淘汰策略(如 LRU/LFU)。这些特性共同确保 Redis 在性能与灵活性之间的平衡。
|
存储 消息中间件 NoSQL
Redis 数据结构与对象
【10月更文挑战第15天】在实际应用中,需要根据具体的业务需求和数据特点来选择合适的数据结构,并合理地设计数据模型,以充分发挥 Redis 的优势。
250 64
|
存储 NoSQL 算法
「Redis」数据结构与对象
Redis数据结构与对象介绍
162 0

热门文章

最新文章