第一篇 Redis常用数据结构介绍及基本操作

2020/11/29 Redis 浏览

目前Redis支持的主要数据结构包含5种,分别是:

  • 字符串(string)
  • 散列(hash)
  • 列表(list)
  • 集合(set)
  • 有序集合(sorted set)

1.1 常用数据结构介绍

1.1.1 字符串(String)

字符串结构是开发人员平常使用最多的结构,同时也是最简单的,它的值不仅仅是字符串,也可以是数值.

常用命令包括:GETSETINCRDECRMGET等等

主要应用场景:字符串是最常见的数据类型之一,普通的键/值存储可以使用该数据结构进行存储.从而可以完全实现当前的Memcached所实现的功能并提高效率。还可以享受Redis的定时持久性,操作日志和复制功能。除了提供GET,SET,INCR,DECR等操作外,Redis还提供以下功能:

  • 获取字符串的长度
  • 字符串的内容进行追加
  • 设置并获取一部分字符串
  • 设置并获取字符串某一个bit
  • 批量设置一系列字符串的内容

常用的方案:使用该数据结构缓存程序计数器,例如:微博数量、粉丝数量等等

1.1.2 散列(Hash)

常用命令:HGETHSETHGETALL

Hash和java语言中的Map集合结构很相似,只不过在Redis中,一个Hash结构需要一个key进行关联,数据结构有点类似于如下图:

上图中黑色加粗的key1所代表的是Redis中存储的键值,而虚线框中的key1所代表的是Hash数据结构中的键值

简单操作如下:

127.0.0.1:6379> hget tenant tenantId
(nil)
127.0.0.1:6379> hset tenant tenantId abc
(integer) 1
127.0.0.1:6379> hset tenant title test
(integer) 1
127.0.0.1:6379> hget tenant tenantId
"abc"
127.0.0.1:6379> hgetall tenant
1) "tenantId"
2) "abc"
3) "title"
4) "test"
127.0.0.1:6379>

假设我们需要存储包含以下信息的用户信息对象数据:

用户ID是要查找的键,储值用户对象包含名称,年龄,生日等信息,如果要以普通的键/值结构进行存储,主要有以下两种存储方法:

  • 第一种方法是将用户的ID作为Redis中的Key,值则是序列号的用户对象,这样做的缺点是在存储和获取时,增加了序列化/反序列化的成本,如果要修改用户的某一个属性时,则需要对整个对象进行操作,在并发条件下,会出现CAS之类的问题.

  • 第二种方法是将多少个该用户信息对象的成员保存到键值数目中,其中用户id +相应属性的名称作为唯一标识符来获取相应属性的值,尽管这样做的代价是序列化和并发性被省略,但是用户ID被重复存储,如果存在大量此类数据,则存在内存大量浪费的情况。

因此,Redis提供的哈希是解决此问题的好方法,Redis哈希实际上是内部存储的值作为哈希图,并提供对映射成员接口的直接访问,例如:

也就是说,键仍然是用户ID,值是地图,地图键是属性名称的成员,值是属性值,以便可以通过其内部地图键直接修改和访问数据(Redis称为内部映射键字段),这意味着可以通过键(用户ID)+字段(属性标签)来操纵相应的属性数据,而无需重复存储数据,也没有序列化和并发修改控制的问题。是一个很好的解决方案。

还需要注意的是,Redis提供了一个可以直接获取所有属性数据的接口(Hgetall),但是如果内部映射具有大量成员,则涉及遍历整个内部映射,这可能很耗时由于Redis单线程模型。其他客户端请求根本没有响应,这需要格外注意。

使用情况:存储部分更改数据,例如用户信息。

1.1.3 列表(list)

Redis的列表结构,存放一个string类型的链表.

常用命令:lpushrpushlpoprpoplrange

应用场景:例如一个网站上的粉丝列表、监控列表、消息队列、日志收集器

list底层是一个双向链接的数据结构,相信自己具有数据结构知识的人应该能够理解其结构。使用列表结构,我们可以轻松实现最新消息排名和其他功能。也可以用作消息队列,你可以使用列表的推操作将任务放置在列表中,然后工作线程使用弹出操作将任务取出。 Redis还提供了用于操作列表的一部分的API,您可以直接查询以从列表的一部分中删除元素。

简单命令操作:

127.0.0.1:6379> lpush list-user uid001
(integer) 1
127.0.0.1:6379> lpush list-user uid002
(integer) 2
127.0.0.1:6379> lrange list-user 0 1
1) "uid002"
2) "uid001"
127.0.0.1:6379> rpush list-user uid003
(integer) 3
127.0.0.1:6379> lrange list-user 0 2
1) "uid002"
2) "uid001"
3) "uid003"
127.0.0.1:6379> lpop list-user
"uid002"
127.0.0.1:6379> llen list-user
(integer) 2
127.0.0.1:6379> lrange list-user 0 2
1) "uid001"
2) "uid003"
127.0.0.1:6379> rpush list-user uid004
(integer) 3
127.0.0.1:6379> llen list-user
(integer) 3
127.0.0.1:6379> lrange list-user 0 2
1) "uid001"
2) "uid003"
3) "uid004"
127.0.0.1:6379> rpop list-user
"uid004"
127.0.0.1:6379> lrange list-user 0 2
1) "uid001"
2) "uid003"
127.0.0.1:6379>   

1.1.4 集合(Set)

Set对外提供的功能类似于列表(list),但是Set更加轻量级(对于内存来说),Set存储的元素是不重复的

也可以根据不同的Set集合取交集、并集等

常用操作命令:saddsmembersscardsintersdiffsrem

常见命令操作:

127.0.0.1:6379> sadd myset a1 a2 a3 a4 a5 a6 a7
(integer) 7
127.0.0.1:6379> smembers myset
1) "a6"
2) "a1"
3) "a2"
4) "a3"
5) "a5"
6) "a7"
7) "a4"
127.0.0.1:6379> 

比如在微博系统中,每一个用户都会有一个关注列表,此时有如下需求:

  • 如何找出两个用户共同关注的人
  • 如何找出用户1与用户2关注的不同的人

假设用户1的关注列表中有:u1、u2、u3、u4、u5、u6

用户2的关注用户列表中有:u2、u5、u7、u8

从上面的结果来看,用户1和用户2共同关注的用户有:u2\u5

用户1关注的不同的人:u1\u3\u4\u6

用户2关注的不同的人:u7\u8

通过Redis的set集合来进行实现,命令如下:

# 先集合初始化 u1set \u2set
127.0.0.1:6379> sadd u1set u1 u2 u3 u4 u5 u6
(integer) 6
127.0.0.1:6379> sadd u2set u2 u5 u7 u8
(integer) 4
127.0.0.1:6379> smembers u1set
1) "u4"
2) "u1"
3) "u2"
4) "u3"
5) "u6"
6) "u5"
127.0.0.1:6379> smembers u2set
1) "u7"
2) "u5"
3) "u8"
4) "u2"
# 找出u1与u2用户关注的不同人的集合
127.0.0.1:6379> sdiff u1set u2set
1) "u3"
2) "u6"
3) "u1"
4) "u4"
# 找出u2与u1用户关注的不同人的集合
127.0.0.1:6379> sdiff u2set u1set
1) "u7"
2) "u8"
# u1\u2共同关注的人
127.0.0.1:6379> sinter u1set u2set
1) "u5"
2) "u2"
127.0.0.1:6379>  

1.1.5 有序集合(Sorted Set)

有序集合的数据结构类似于集合与哈希之间的混合体,与集合一样,其元素是由唯一非重复的字符串组成.

但是集合(Set)中的元素没有排序,而有序集合排序后,其中的每个元素都与一个浮点数值相关联(这也是为什么说该数据结构与哈希结构类似的原因,因为其每个元素都映射了一个排序值)

排序规则如下:

  • 如果A和B是两个分数不同的元素,如果A.score>B.store,那么A>B
  • 如果A和B的分数完全相同,由于A字符串在字典上大于B字符串,所以A>B。AB不能相等,因为有序集合的元素具有唯一性

常用操作命令:zaddzrangezrankzrangebyscorezcard

简单操作命令:

127.0.0.1:6379> zadd mysort 10 ab 40 cb 60 db
(integer) 3
127.0.0.1:6379> zrange mysort 0 2
1) "ab"
2) "cb"
3) "db"
127.0.0.1:6379> zadd mysort 55 eb
(integer) 1
127.0.0.1:6379> zrange mysort 0 3
1) "ab"
2) "cb"
3) "eb"
4) "db"
127.0.0.1:6379> zcard mysort
(integer) 4
127.0.0.1:6379>                  

1.2 Java语言Redis客户端介绍及操作

Java语言中使用Redis比较常见的客户端:jedisRedissonlettuce

客户端 GitHub star 说明
jedis https://github.com/xetorthio/jedis 9.4K 轻量级的java客户端
lettuce https://github.com/lettuce-io/lettuce-core 3.6K 高级Redis客户端,用于线程安全的同步,异步和反应式使用。 支持群集,前哨,流水线和编解码器。
Redisson https://github.com/redisson/redisson 14.6k 分布式以及可伸缩性更强

目前使用的Spring Boot框架底层主要提供了jedis、lettuce两种支持,开发者通过yml中进行配置,Spring Boot自动选择,Spring Boot框架目前默认使用lettuce作为Redis的客户端。

以下主要是通过以上的客户端java库,对Redis的基本数据类型进行简单的操作

1.2.1 使用Jedis进行操作

通过Jedis操作Redis,根据Jedis提供的构造函数,主要有三种方式获取Jedis对象的实例

  • 根据Redis的ip地址、端口简单创建获取实例
  • 根据连接池配置获取Redis进行操作(推荐做法)
  • 根据集群配置创建Jedis实例获取Redis的操作对象

先来看第一种简单的创建Jedis对象的方式,代码如下:

//Redis的ip及端口号
Jedis simple=new Jedis("localhost",6379);
//如果Redis设置了密码,此处需要设置密码,反之则不用
simple.auth("123456");

创建JedisPool连接池对象,从连接池获取Jedis实例,代码如下:

GenericObjectPoolConfig poolConfig=new GenericObjectPoolConfig();
//连接池中的最大空闲连接
poolConfig.setMaxIdle(8);
//连接池最大连接数(使用负值表示没有限制)
poolConfig.setMaxTotal(8);
// 连接池中的最小空闲连接
poolConfig.setMinIdle(0);
//连接池最大阻塞等待时间(使用负值表示没有限制)
poolConfig.setMaxWaitMillis(-1);
//jedisPool只需要初始化1次即可,每次获取Jedis直接调用getResource方法从连接池中获取
JedisPool jedisPool=new JedisPool(poolConfig,"localhost",6379,5000,"123456");
Jedis jedis=jedisPool.getResource();

如果我们的Redis是集群部署,此时,我们可以通过集群的配置获取Jedis对象以操作Redis,代码如下:

//创建连接池对象
GenericObjectPoolConfig poolConfig=new GenericObjectPoolConfig();
//连接池中的最大空闲连接
poolConfig.setMaxIdle(8);
//连接池最大连接数(使用负值表示没有限制)
poolConfig.setMaxTotal(8);
// 连接池中的最小空闲连接
poolConfig.setMinIdle(0);
//连接池最大阻塞等待时间(使用负值表示没有限制)
poolConfig.setMaxWaitMillis(-1);
//创建集群
Set<HostAndPort> nodes=new HashSet<>();
HostAndPort hostAndPort=new HostAndPort("192.168.0.11",133);
HostAndPort hostAndPort1=new HostAndPort("192.168.0.12",133);
nodes.add(hostAndPort);
nodes.add(hostAndPort1);
//1、connectionTimeout 连接超时时间
//2、soTimeout 读取数据超时时间
//3、maxAttempts 错误时最大尝试次数
//4、password 密码
//创建集群对象
JedisCluster jedisCluster=new JedisCluster(nodes,2000,20000,3,"123456",poolConfig);

不管是集群方式或者是连接池等,最终操作Redis的数据结构在客户端都是差不多的,接下来以Jedis为例来操作Redis的五种不同数据结构

字符串(String)

常用命令包括:GETSETINCRDECRMGET等等

字符串操作直接看以下代码:

Jedis jedis=getSimpleJedis();
//获取string
String value=jedis.get("answertoken_abc");
System.out.println(value);
//设置string类型的key-value
jedis.set("mykey","myvalue");
//根据SetParams设置,主要包含四种:ex\nx\xx\px
//1、ex:设置键key的过期时间,单位时秒
//2、px:设置键key的过期时间,单位毫秒

//3、nx:只有键key不存在的时候才会设置key的值
//4、xx:只有键key存在的时候才会设置key的值

//第一种情况,设置一个key值,同时指定过期时间
jedis.set("mykey","myvalue1",SetParams.setParams().ex(10));
//第二种情况,当key值不存在时进行设置,并且设置过期时间
jedis.set("mykey","myvalue2",SetParams.setParams().nx().ex(10));
//第三种情况,当key值存在时进行设置,并且设置过期时间(分布式锁场景)
jedis.set("mykey","myvalue3",SetParams.setParams().xx().ex(10));
//自增操作
jedis.set("mykey","1");
//自增+1
jedis.incr("mykey");
//自增+n
jedis.incrBy("mykey",10);
System.out.println(jedis.get("mykey"));
//自减操作
jedis.decr("mykey");
//自减-N
jedis.decrBy("mykey",10);
System.out.println(jedis.get("mykey"));

通过上面jedis对象对string结构提供的API方法进行操作还是非常方便的。

散列(hash)

常用命令:HGETHSETHGETALL

在Jedis客户端中操作Redis的hash结构和在命令行中差不多,jedis封装的api命令几乎没有什么区别

java开发者在操作时,一个hash理解为在Java语言中的Map数据结构即可。

//操作hash
Jedis jedis=getSimpleJedis();
//赋值操作        
String hashKey="myhashkey";
Map<String,String> values=new HashMap<>();
values.put("name","张三");
values.put("age","13");
//批量一次性设置has的值
jedis.hset(hashKey,values);
//单个设置
jedis.hset(hashKey,"title","Jedis牛逼");
//get操作
//获取单个
String hvalue=jedis.hget(hashKey,"name");
//获取整个
Map<String,String> hvalues=jedis.hgetAll(hashKey);
//删除操作
//提供可变数组,删除多个hash中的key值
String[] hdelKeys=new String[]{"age","name"};
//删除某hash中的key
jedis.hdel(hashKey,hdelKeys);
//删除整个hash
jedis.del(hashKey);

需要注意的是,针对批量设置hash值的操作,需要Redis版本大于等于4.0.0版本

As of Redis 4.0.0, HSET is variadic and allows for multiple field/value pairs.

列表(list)

列表中常用命令:lpushrpushlpoprpoplrange

//操作list
Jedis jedis=getSimpleJedis();
String listKey="mylistkey";
String[] listValues=new String[]{"a","b","c"};
//放入集合中
jedis.lpush(listKey,listValues);
//根据区间获取集合中的元素集合
List<String> rangeValues=jedis.lrange(listKey,0,2);
rangeValues.forEach(s -> System.out.println(s));
//从左侧链表取出一个值
String lvalue=jedis.lpop(listKey);
//此处值应该是c
System.out.println("lpopValue:"+lvalue);
String rvalue=jedis.rpop(listKey);
//此处值应该是a
System.out.println("rpopValue:"+rvalue);
//删除
jedis.del(listKey);

集合(Set)

常用操作命令:saddsmembersscardsintersdiffsrem

集合(Set)和列表相比较而言,不会存在重复的值。

//操作Set
Jedis jedis = getSimpleJedis();
String setKey="mysetKey";
//此操作最终只会添加成功3个元素,因为存在abc重复
jedis.sadd(setKey,"abc","ddd","aaa","abc");
Set<String> stringSet=jedis.smembers(setKey);
stringSet.stream().forEach(s -> System.out.println("SetValue:"+s));
String setKey1="mysetKey1";
//添加第二个集合
jedis.sadd(setKey1,"ab1c","ddd","aaa","2abc");
//数量
long set1Number=jedis.scard(setKey1);
System.out.println("集合2数量:"+set1Number);
//比较第1个集合与其它集合的区别元素
Set<String> diffSets=jedis.sdiff(setKey,setKey1);
diffSets.stream().forEach(s -> System.out.println("diffValue:"+s));
//取交集
Set<String> sinters=jedis.sinter(setKey,setKey1);
sinters.stream().forEach(s -> System.out.println("sinterValue:"+s));
//取并集
Set<String> sunions=jedis.sunion(setKey,setKey1);
sunions.stream().forEach(s -> System.out.println("sunionsValue:"+s));
//获取两个集合的并集
jedis.del(setKey,setKey1);

有序集合(Sorted Set)

常用操作命令:zaddzrangezrankzrangebyscorezcard

//操作Set
Jedis jedis = getSimpleJedis();
String zsetKey="myzsetKey";
jedis.zadd(zsetKey,10,"abc");
//多个一起添加
Map<String,Double> zvalues=new HashMap<>();
zvalues.put("c1",230D);
zvalues.put("c2",110D);
zvalues.put("c3",130D);
zvalues.put("c4",21D);
jedis.zadd(zsetKey,zvalues);
//统计数量
long size=jedis.zcard(zsetKey);
System.out.println("数据量:"+size);
//获取
Set<String> stringSet=jedis.zrange(zsetKey,0,2);
stringSet.stream().forEach(s -> System.out.println(s));
//倒序
System.out.println("倒序");
stringSet=jedis.zrevrange(zsetKey,0,2);
stringSet.stream().forEach(s -> System.out.println(s));
jedis.del(zsetKey);
jedis.close();

以上操作仅仅只是部分api的展示,更多的操作开发者需要自行探索,结合Redis的命令介绍,才能更多的加深理解。

1.2.2 使用Lettuce操作Redis

lettuce可能大家不常见,但是我们基于Spring Boot来操作的StringRedisTemplate大家肯定熟悉,如果开发者默认没有选择,那么StringRedisTemplate底层依赖的就是lettuce来作为默认依赖,最终来操作Redis。

站内搜索

    Table of Contents