Redis日记(day02):Redis-key、5大基本数据类型,特殊数据类型介绍

194 阅读36分钟

第二章:Redis键、5大基本数据类型,特殊数据类型介绍

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker. Redis provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions, and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster

Redis 是一个开源(BSD 许可)的内存数据结构存储,用作数据库、缓存和消息代理。Redis 提供数据结构,例如字符串、散列、列表、集合、具有范围查询的排序集合、位图、超日志、地理空间索引和流。Redis 具有内置复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久性,并通过 Redis Sentinel 和 Redis Cluster 自动分区提供高可用性

第1节:Redis键(key)

1.1、Redis键key概述

在Redis中,有很多Redis键命令,可以用于管理 redis 的键。key就相当于是下面五大数据类型对应的名称

语法:

Redis 键命令的基本语法如下:

redis 127.0.0.1:6379> COMMAND KEY_NAME 

实例

redis 127.0.0.1:6379> SET runoobkey redis
OK
redis 127.0.0.1:6379> DEL runoobkey
(integer) 1

在以上实例中 DEL 是一个命令, runoobkey 是一个键,redis是它对应的一个Value, 如果键被删除成功,命令执行后输出 (integer) 1 ,否则将输出 (integer) 0

1.2、Redis键基本命令

下表给出了与 Redis 键相关的基本命令:

序号命令及描述
0keys * 查看当前库(默认0号库)所有key (匹配:keys 1)
1DEL key 该命令用于在 key 存在时删除指定的 key。
2DUMP key 序列化给定 key ,并返回被序列化的值。
3EXISTS key 检查给定 key 是否存在。
4EXPIRE key seconds 为给定 key 设置过期时间,以秒计。
5EXPIREAT key timestamp EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。
6PEXPIRE key milliseconds 设置 key 的过期时间以毫秒计。
7PEXPIREAT key milliseconds-timestamp 设置 key 过期时间的时间戳(unix timestamp) 以毫秒计
8KEYS pattern 查找所有符合给定模式( pattern)的 key 。
9MOVE key db 将当前数据库的 key 移动到给定的数据库 db 当中。
10PERSIST key 移除 key 的过期时间,key 将持久保持。
11PTTL key 以毫秒为单位返回 key 的剩余的过期时间。
12TTL key 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。-1表示永元存活,-2表示已经消失了
13RANDOMKEY 从当前数据库中随机返回一个 key 。
14RENAME key newkey 修改 key 的名称
15RENAMENX key newkey 仅当 newkey 不存在时,将 key 改名为 newkey 。
16SCAN cursor [MATCH pattern] [COUNT count] 迭代数据库中的数据库键。
17TYPE key 查看 key 所储存的值的类型。

更多命令请参考: redis.io/commands

测试:

127.0.0.1:6379> keys	# 查看所有的key (empty list or set)
127.0.0.1:6379> set name kuangshen	# set key OK
127.0.0.1:6379> keys *
1) "name" 127.0.0.1:6379> set age 1 OK
127.0.0.1:6379> keys
1)"age"
2)"name"
127.0.0.1:6379> EXISTS name	# 判断当前的key是否存在
(integer) 1
127.0.0.1:6379> EXISTS name1
(integer) 0
127.0.0.1:6379> move name 1	# 移除当前的key
(integer) 1
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> set name qinjiang OK
127.0.0.1:6379> keys
1)"age"
2)"name" 127.0.0.1:6379> clear 127.0.0.1:6379> keys
1)"age"
2)"name" 127.0.0.1:6379> get name "qinjiang"
127.0.0.1:6379> EXPIRE name 10	# 设置key的过期时间,单位是秒
(integer) 1
127.0.0.1:6379> ttl name	# 查看当前key的剩余时间
(integer) 4
127.0.0.1:6379> ttl name
(integer) 3
127.0.0.1:6379> ttl name
(integer) 2
127.0.0.1:6379> ttl name
(integer) 1
127.0.0.1:6379> ttl name
(integer) -2   #-2表示已经消失了
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> type name	# 查看当前key的一个类型!
string
127.0.0.1:6379> type age string

补充一些其他常用命令:

unlink key   根据value选择非阻塞删除,仅将key从keyspace元数据中删除,真正的删除会在后续异步操作。
expire key 10   10秒钟:为给定的key设置过期时间(生存时间)
select命令切换数据库
dbsize  查看当前数据库的key的数量
flushdb  清空当前库
flushall  清空全部库

第2节:5大数据类型--字符串类型

2.1、Redis字符串 (String)

简介:String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。一个Redis中字符串value最多可以是512M

Redis 字符串数据类型的相关命令用于管理 redis 字符串值,基本语法如下:

redis 127.0.0.1:6379> COMMAND KEY_NAME

实例演示set key value

redis 127.0.0.1:6379> flushdb  #清空
redis 127.0.0.1:6379> SET runoobkey redis #runoobkey不存在,就是添加,可以加双引号
OK redis 
127.0.0.1:6379> GET runoobkey  #取出key对应的值
"redis"
127.0.0.1:6379> SET runoobkey Memcached  #对key的value重新设置
127.0.0.1:6379> GET runoobkey  
"Memcached"  #key已经存在,覆盖原来的value

在以上实例中我们使用了 SETGET 命令,键为 runoobkey

2.2、Redis字符串相关命令

下表列出了常用的 redis 字符串命令:

序号命令及描述
1SET key value 设置指定 key 的值,可以覆盖已经存在的key,如果key不存在,就直接创建新的key和value
2GET key 获取指定 key 的值。
3GETRANGE key start end 返回 key 中字符串值的子字符 start 、end是索引,起始值为0
4GETSET key value 将给定 key 的值设为新值 value ,并返回 key 的旧值(old value)。
5GETBIT key offset 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。
6MGET key1 [key2..] 获取所有(一个或多个)给定 key 的值。
7SETBIT key offset value 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
8SETEX key seconds value 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。
9SETNX key value 只有在 key 不存在时设置 key 的值。Key存在时返回0表示不能设置成功
10SETRANGE key offset value 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。起始值为0
11STRLEN key 返回 key 所储存的字符串值的长度。
12MSET key value [key value ...] 同时设置一个或多个 key-value 对。
13MSETNX key value [key value ...] 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。这是一个原子性的操作,有一个失败则都失败,即必须所有key 都不存在
14PSETEX key milliseconds value 这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。
15INCR key 将 key 中储存的数字值增一。只能对数字值操作,如果为空,新增值为1
16INCRBY key increment 将 key 所储存的值加上给定的增量值(increment) 。自定义步长增量
17INCRBYFLOAT key increment 将 key 所储存的值加上给定的浮点增量值(increment) 。
18DECR key 将 key 中储存的数字值减一。只能对数字值操作,如果为空,新增值为-1
19DECRBY key decrement key 所储存的值减去给定的减量值(decrement) 。
20APPEND key value 如果 key 已经存在并且Value是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。

更多命令请参考: redis.io/commands

相关命令的演示:

1、APPEND key value

redis 127.0.0.1:6379> flushdb  
redis 127.0.0.1:6379> SET runoobkey redis 
OK redis 
127.0.0.1:6379> GET runoobkey  
"redis"
127.0.0.1:6379> APPEND runoobkey abc #将指定的 value 追加到该 key 原来值(value)的末尾
8        #返回key对应value值的总长度,Redisabc
127.0.0.1:6379> GET runoobkey  
"redisabc"   

2、INCR keyDECR keyINCRBY key increment

redis 127.0.0.1:6379> SET v1 0 
OK
127.0.0.1:6379> GET v1  
"0"
127.0.0.1:6379> INCR v1 #将 key 中储存的数字值增一
(integer)1        
127.0.0.1:6379> GET v1  
"1"  
127.0.0.1:6379> decr v1 #将 key 中储存的数字值增一
(integer)0        
127.0.0.1:6379> INCRBY v1 10 #将 key 所储存的值加上给定的增量值(increment)
(integer)10 

3、GETRANGE key start end :返回 key 中字符串值的子字符 start 、end是索引,起始值为0

#截取字符串
redis 127.0.0.1:6379> SET key1 "hello,world" 
OK
127.0.0.1:6379> GET key1  
"hello,world"
127.0.0.1:6379> GETRANGE key1 0 3
"hell"
127.0.0.1:6379> GETRANGE key1 0 -1
"hello,world" 

4、SETRANGE key offset value :用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。起始值为0

#类似于替换
redis 127.0.0.1:6379> SET key2 "abcdefg" 
OK
127.0.0.1:6379> GET key2  
"abcdefg"
127.0.0.1:6379> SETRANGE key2 1 @@
(integer)7  
127.0.0.1:6379> GET key2
"a@@defg"

5、SETEX key seconds valueSETNX key value

  • SETEX key seconds value将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。
  • SETNX key value 只有在 key 不存在时设置 key 的值。Key存在时返回0表示不能设置成功
redis 127.0.0.1:6379> SETEX key3 30 "hello" 
OK
127.0.0.1:6379> ttl key3  
(integer)27  
127.0.0.1:6379> GET key3  #30s之后就取不到了
"hello" 
127.0.0.1:6379> SETNX key4 "redis"
(integer)1   #设置成功

127.0.0.1:6379> SETNX key2 "mysql"
(integer)0   #key2已经存在,设置失败

6、其他,参照上面命令

MSET key value [key value ...]   #同时设置一个或多个 key-value 对。
MGET key1 [key2..]    #获取所有(一个或多个)给定 key 的值。
MSETNX key value [key value ...]   #同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。这是一个原子性的操作,有一个失败则都失败,即必须所有key 都不存在

7、设置对象

#这里的key是一个巧妙的设计:user:{id}:{field}
127.0.0.1:6379>set user:1 {name:zhangsan,age:3}  #设置一个user:1对象为json字符串来保存一个对象
或
127.0.0.1:6379>MSET user:1:name zhangsan  user:1:age 3 
ok
127.0.0.1:6379>MGET user:1:name  user:1:age 3 
1)"zhangsan"
2)"3"

8、GETSET key value :将给定 key 的值设为新值 value ,并返回 key 的旧值(old value)

127.0.0.1:6379>getset db redis  #如果不存在值,则返回nil
(nil)
127.0.0.1:6379>get db
"redis"
127.0.0.1:6379>getset db yyy #如果存在值,返回原来的值,并设置新的值
"redis"
127.0.0.1:6379>get db
"yyy"

2.4、String类型的使用场景

Value除了是我们的字符串,还可以是数字!

  • 计数器
  • 统计多单位的数量 key-value = uid:9234875:follow 0(incr)[只是缓存在了内存],需要mysql等做持久化
  • 粉丝数
  • 对象缓存存储

2.5、原子性

在上面一些命令中,可以看到有些命令强调原子性,比如MSETNX key value [key value ...] 或者INCR key,这些都是是属于原子操作,那什么叫原子操作呢?

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch (上下文切换,切换到另一个线程)。

  1. 在单线程中,能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间。比如运行指令INCR key就是对存储在指定 key 中的数值执行原子的加1操作
  2. 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
  3. Redis单命令的原子性,即一条指令就能完成操作,主要得益于Redis的单线程。

案例:java中的i++是否是原子操作?

答案:i++不是原子操作,也就是说,它不是单独一条指令,而是4条指令:

这就要讲到在Java代码在JVM上的执行过程了。在JVM中,i++操作分为下面4条指令流,我们来看一下它的字节码文件反编译后的结果:

GETSTATIC org/example/basic/TestIpp.i : I     // 读取 i 的值,入栈
ICONST_1                                      // 将数值 1 入栈
IADD                                          // 取出栈顶的 2 个元素求和,并将结果压入栈中
PUTSTATIC org/example/basic/TestIpp.i : I     // 从栈顶取值,存入 i 中

从字节码文件反编译后的结果可以看出,i++ 的操作过程的前两步执行完,栈中应该是如图(1)所示的:

因此如果是单线程操作,i++毫无问题;根据字节码指令在栈中执行,依次执行就是了,但是在多核处理器上,用多线程来做i++会有什么问题呢?举例具体解析:给定i=0,创建A、B两个线程,这两个线程分别对 i 进行++100次,值是多少?

  • i变量共享
  • 两个线程分别对应各自的虚拟机栈
  • 在i++的执行的4条指令中,第三条指令就是取出栈顶的两个元素求和再入栈,这里就是出现线程安全问题的原因,在第二步之后,如果再有第二个线程执行i++操作到第一步时,就会出现资源抢占造成并发问题(没有锁的情况下)。具体如图解所示
  • 然后就是各种资源的抢占,栈顶的相加操作,并将结果写入栈。通过最后结果,可知最小的结果 i = 2 所以,本题的结果在 2~200 之间

分析:

  • 最大值:200,即第一个线程加100次,第二个线程随后又加100次,依次执行
  • 最小值: 多线程下会导致最终i的值可能仅为2。

图解: 假设两个线程的执行步骤如下,对A和B来说,i的初始值都是0(不考虑原子操作)

image.png

2.6、Redis的String类型底层数据结构

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配

image.png

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串实际长度大于原有len的时候【比如对字符串进行追加操作后】,就会进行扩容,当字符串原实际长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

第3节:5大数据类型--Redis列表(List)

3.1、Redis列表(List)

简介:Redis列表是单键多值,即一个键Key对应存储多个值Value,Redis 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

image.png

redis 127.0.0.1:6379> LPUSH runoobkey redis 
(integer) 1
redis 127.0.0.1:6379> LPUSH runoobkey mongodb 
(integer) 2 
redis 127.0.0.1:6379> LPUSH runoobkey mysql 
(integer) 3 
redis 127.0.0.1:6379> LRANGE runoobkey 0 -1  
1) "mysql" 
2) "mongodb" 
3) "redis"

在以上实例中我们使用了 LPUSH 将三个值插入了名为 runoobkey 的列表(而且都是从队头插入)当中。并且获取到的元素顺序是先进后出的,类似队列

3.2、Redis 列表命令

下表列出了列表相关的基本命令:

序号命令及描述
1BLPOP key1 [key2 ] timeout 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
2BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
3BRPOPLPUSH source destination timeout 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
4LINDEX key index 通过索引下标获取指定列表中的元素 (从左到右)
5LINSERT key BEFORE|AFTER pivot value 在列表的元素pivot前或者后插入新元素value
6LLEN key 获取列表长度
7LPOP key 移出并获取列表的第一个元素 值在键在,值光键亡。
8LPUSH key value1 [value2] 将一个或多个值插入到列表头部
9LPUSHX key value 将一个值插入到已存在的列表头部
10LRANGE key start stop 获取列表指定范围内的元素,0表示左边第一个(头),-1表示右边第一个(尾),(0~-1表示获取所有)
11LREM key count value 从左边删除指定列表的n个value(从左到右精确匹配)
12LSET key index value 通过索引设置列表元素的值,将列表key下标为index的值替换成value
13LTRIM key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
14RPOP key 移除列表的最后一个元素【尾部】,返回值为移除的元素。值在键在,值光键亡。
15RPOPLPUSH source destination 移除Key值为source对应的列表的最后一个元素【尾部】,并将该元素添加到key值为destination的另一个列表头部,并返回
16RPUSH key value1 [value2] 在列表中添加一个或多个值到列表尾部
17RPUSHX key value 为已存在的列表添加值

3.3、数据结构

  • List实际上是一个链表,before Node after,left,right都可以插入值
  • 如果key不存在,创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有值,就成为空链表,即代表不存在
  • 在两边插入或者改动值,效率最高,中间元素,相对来说效率低一点
  • 可以灵活改变,比如应用在消息队列,如果是(Lpush Rpop),则成为队列如果是(Lpush Lpop),则成为栈。

List的数据结构为快速链表quickList。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist

因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如下面这个列表里存的只是int类型的数据,普通链表结构上还需要两个额外的指针prevnext

image.png

Redis将普通链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

第4节:5大数据类型--Redis集合(Set)

4.1、Redis集合(Set)

简介

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Set集合成员是唯一的,这就意味着集合中不能出现重复的数据。集合对象的编码可以是 intset 或者 hashtable。Redis 的 Set 是 String 类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。一个算法,随着数据的增加,执行时间的长短,如果是O(1),意味着数据增加,查找数据的时间不变

集合中最大的成员数为 2^32 - 1 (4294967295,每个集合可存储40多亿个成员)。

实例

redis 127.0.0.1:6379> SADD runoobkey redis 
(integer) 1 
redis 127.0.0.1:6379> SADD runoobkey mongodb 
(integer) 1 
redis 127.0.0.1:6379> SADD runoobkey mysql 
(integer) 1 
redis 127.0.0.1:6379> SADD runoobkey mysql 
(integer) 0 
redis 127.0.0.1:6379> SMEMBERS runoobkey  
1) "mysql" 
2) "mongodb" 
3) "redis"

在以上实例中我们通过 SADD 命令向名为 runoobkey 的集合插入的三个元素。

4.2、Redis 集合命令

下表列出了 Redis 集合基本命令

序号命令及描述
1SADD key member1 [member2] 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略,即返回(integer) 0
2SCARD key 获取集合的成员数
3SDIFF key1 [key2] 返回第一个集合与其他集合之间的差集元素。(key1中的,不包含key2中的)
4SDIFFSTORE destination key1 [key2] 返回给定所有集合的差集并存储在 destination 中
5SINTER key1 [key2] 返回给定所有集合的交集元素
6SINTERSTORE destination key1 [key2] 返回给定所有集合的交集并存储在 destination 中
7SISMEMBER key member判断 member 元素是否是集合 key 的成员
8SMEMBERS key 返回集合中的所有成员
9SMOVE source destination member 将 member 元素从 source 集合移动到 destination 集合
10SPOP key 移除并返回集合中的一个随机元素
11SRANDMEMBER key [count] 随机从该集合中取出count个值。不会从集合中删除 。
12SREM key member1 [member2] 移除集合中一个或多个成员
13SUNION key1 [key2] 返回所有给定集合的并集元素
14SUNIONSTORE destination key1 [key2] 所有给定集合的并集存储在 destination 集合中
15SSCAN key cursor [MATCH pattern] [COUNT count] 迭代集合中的元素

4.3、数据结构

Set数据结构是dict字典,字典是用哈希表实现的。

Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

比如说关注的人放在一个set集合中,共同关注的人可以做个交集,共同好友、二度好友、推荐好友(六度分割理论)

第5节:5大数据类型--哈希(Hash)

5.1、hash概述

简介

Redis hash 是一个键值对集合。Redis hash是一个string类型的field(字段) 和 value(值)的映射表,hash特别适合用于存储对象(String更加适合字符串数据存储)。类似Java里面的Map,其实可以把hash就理解为一个Map集合,即key-map形式,value值是一个Map集合,Redis 中每个 hash 可以存储 2^32 - 1 键值对(40多亿)。

127.0.0.1:6379>  HMSET runoobkey name "redis tutorial" description "redis basic commands for caching" likes 20 visitors 23000 
OK 
127.0.0.1:6379>  HGETALL runoobkey 
1) "name" 
2) "redis tutorial" 
3) "description" 
4) "redis basic commands for caching" 
5) "likes" 
6) "20" 
7) "visitors" 
8) "23000"

在以上实例中,我们设置了 redis 的一些描述信息(name, description, likes, visitors) 到哈希表的 runoobkey 中。

hash类型适合hash变更的数据:用户信息之类的。经常变动

# HSET key field value
HSET user:1 name age

5.2、Redis hash 命令

下表列出了 redis hash 基本的相关命令:

序号命令及描述
1HDEL key field1 [field2] 删除一个或多个哈希表字段
2HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。
3HGET key field 获取存储在哈希表中指定字段的值。
4HGETALL key 获取在哈希表中指定 key 的所有字段和值
5HINCRBY key field increment 为哈希表 key 中的指定字段的整数值加上增量 increment 。
6HINCRBYFLOAT key field increment 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
7HKEYS key 获取所有哈希表中的字段
8HLEN key 获取哈希表中字段的数量
9HMGET key field1 [field2] 获取所有给定字段的值
10HMSET key field1 value1 [field2 value2 ] 同时将多个 field-value (域-值)对设置到哈希表 key 中。
11HSET key field value 将哈希表 key 中的字段 field 的值设为 value 。
12HSETNX key field value 只有在字段 field 不存在时,设置哈希表字段的值。
13HVALS key 获取哈希表中所有值。
14HSCAN key cursor [MATCH pattern] [COUNT count] 迭代哈希表中的键值对。

更多命令请参考:redis.io/commands

举例:

用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:

image.png

5.3、数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable

第6节:Redis有序集合Zset(sorted set)

6.1、Zset概述

简介

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个元素都关联了一个double 类型的 评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。

实例

redis 127.0.0.1:6379> ZADD runoobkey 1 redis 
(integer) 1 
redis 127.0.0.1:6379> ZADD runoobkey 2 mongodb 
(integer) 1 
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql 
(integer) 1 
redis 127.0.0.1:6379> ZADD runoobkey 3 mysql 
(integer) 0 
redis 127.0.0.1:6379> ZADD runoobkey 4 mysql 
(integer) 0 
redis 127.0.0.1:6379> ZRANGE runoobkey 0 10 WITHSCORES  
1) "redis" 
2) "1" 
3) "mongodb" 
4) "2" 
5) "mysql" 
6) "4"

在以上实例中我们通过命令 ZADD 向 redis 的有序集合中添加了三个值并关联上分数。

6.2、Redis 有序集合命令

下表列出了 redis 有序集合的基本命令:

序号命令及描述
1ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员和分数,或者更新已存在成员的分数
2ZCARD key 获取有序集合的成员数
3ZCOUNT key min max 计算在有序集合中指定区间分数的成员数
4ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
5ZINTERSTORE destination numkeys key [key ...] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中
6ZLEXCOUNT key min max 在有序集合中计算指定字典区间内成员数量
7ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合指定区间内的成员 带WITHSCORES,可以让分数一起和值返回到结果集。
8ZRANGEBYLEX key min max [LIMIT offset count] 通过字典区间返回有序集合的成员
9ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
10ZRANK key member 返回有序集合中指定成员的排名, 从0开始。
11ZREM key member [member ...] 移除有序集合中的一个或多个成员
12ZREMRANGEBYLEX key min max 移除有序集合中给定的字典区间的所有成员
13ZREMRANGEBYRANK key start stop 移除有序集合中给定的排名区间的所有成员
14ZREMRANGEBYSCORE key min max 移除有序集合中给定的分数区间的所有成员
15ZREVRANGE key start stop [WITHSCORES] 返回有序集中指定区间内的成员,通过索引,分数从高到低,可以0~-1获取全部
16ZREVRANGEBYSCORE key max min [WITHSCORES] 返回有序集中指定分数区间内的成员,分数从高到低排序
17ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
18ZSCORE key member 返回有序集中,成员的分数值
19ZUNIONSTORE destination numkeys key [key ...] 计算给定的一个或多个有序集的并集,并存储在新的 key 中
20ZSCAN key cursor [MATCH pattern] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素分值)

案例:如何利用zset实现一个文章访问量的排行榜?

redis 127.0.0.1:6379> ZADD topn 1000 v1 2000 v2 3000 v3 
(integer) 3 
redis 127.0.0.1:6379> ZREVRANGE topn 0 9 withscores #降序排列
1) "v3" 
2) "3000" 
3) "v2" 
4) "2000" 
5) "v1" 
6) "1000" 

6.3、数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表

zset底层使用了两个数据结构

  1. hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值
  2. 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

跳跃表(跳表)

1、简介

有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。

2、实例

对比有序链表和跳跃表,从链表中查询出51

(1)有序链表

image.png

要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较,虽然可以二分法实现

(2)跳跃表

image.png

  • 从第2层开始,1节点比51节点小,向后比较。
  • 21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层
  • 在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下
  • 在第0层,51节点为要查找的节点,节点被找到,共查找4次。
  • 从此可以看出跳跃表比有序链表效率要高

第7节:特殊数据类型之Geospatial

7.1、简介

Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离计算查询,经纬度Hash等常见操作。

7.2、命令

geoadd

格式

geoadd [longitude latitude member...]添加地理位置(经度,纬度,名称)

127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai  #经度121.47  维度:31.23 
(integer) 1 
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai 106.50 29.53 chongqing 114.05 22.52 shenzhen 
(integer) 2	#因为我们之前已经添加过上海了,所以shenzhen和chongqing成功了  就是2个

两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。

  • 有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。
  • 当坐标位置超出指定范围时,该命令将会返回一个错误。

已经添加的数据,是无法再次往里面添加的。

geopos

格式

geopos [member...] 获得指定地区的坐标值

127.0.0.1:6379> geopos china:city chongqing 
1) "106.49999767541885376"  
2) "29.52999957900659211"

geodist

格式

geodist [m|km|ft|mi ]获取两个位置之间的直线距离

127.0.0.1:6379> geodist china:city chongqing shanghai km 
"1447.6737"

单位:

  • m 表示单位为米[默认值]。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位

georadius

格式

georadius< longitude>radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径radius内的元素

127.0.0.1:6379> georadius china:city 110 30 1000 km 
1) "chongqing" 
2) "shenzhen"

georadiusbymember

georadiusbymember 和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 georadiusbymember 的中心点是由给定的位置元素决定的, 而不是使用经度和纬度来决定中心点。

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

实例:

redis> GEOADD Sicily 13.583333 37.316667 "Agrigento"
(integer) 1
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEORADIUSBYMEMBER Sicily Agrigento 100 km
1) "Agrigento"
2) "Palermo"
redis>

geohash

Redis GEO 使用 geohash字符串 来保存地理位置的坐标,geohash 用于获取一个或多个位置元素的 geohash 值。

geohash 语法格式如下:

GEOHASH key member [member ...]

实例:

127.0.0.1:6379> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
127.0.0.1:6379> GEOHASH Sicily Palermo Catania
1) "sqc8b49rny0"
2) "sqdtr74hyu0"
redis>

如果两个字符串越接近,则距离越近,GEO底层实现原理其实就是Zset,我们可以使用Zset命令来操GEO,如下:

127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqing" 
2) "shenzhen"

#geo并没有提供删除地理位置的命令,但是可以使用zset集合来操作
127.0.0.1:6379> zrem china:city shenzhen
(integer)1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqing" 

参考:www.runoob.com/redis/redis…

第8节:特殊数据类型之HyperLogLog

8.1、简介

在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。

但像UV(UniqueVisitor,独立访客。一个人多次访问一个网站,但还是算作一个人)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题【有些用户多次访问,但是我们只需要计数一次】。

解决基数问题有很多种方案:

  • 数据存储在MySQL表中,使用distinct count计算不重复个数
  • 使用Redis提供的hash、set、bitmaps等数据结构来处理

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

什么是基数?

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数为5。 即去重后集合的元素个数就是基数,基数估计就是在误差可接受的范围内,快速计算基数。

8.2、命令

序号命令及描述
1PFADD key element [element ...] 添加指定元素到 HyperLogLog 中。
2PFCOUNT key [key ...] 返回给定 HyperLogLog 的基数估算值。
3PFMERGE destkey sourcekey [sourcekey ...] 将多个 HyperLogLog 合并为一个HyperLogLog,存储在destkey中

1、pfadd

pfadd <key>< element> [element ...] 添加指定元素到 HyperLogLog 中

127.0.0.1:6379> pfadd program "java" "c++" "php" 
(integer) 1 
127.0.0.1:6379> pfadd program "java"  #因为已经有了java元素,就添加失败返回0 
(integer) 0

将所有元素添加到指定HyperLogLog数据结构中。如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0。

2、pfcount

pfcount<key> [key ...] 计算program的近似基数 ,可以计算多个program,比如用program存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可

127.0.0.1:6379> pfcount program  
(integer) 3 	#java c++ php

3、pfmerge

pfmerge<destkey><sourcekey> [sourcekey ...] 将一个或多个program合并后的结果存储在另一个program中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得

127.0.0.1:6379> pfadd k1 a1 b c   #设置k1 
(integer) 1 
127.0.0.1:6379> pfcount k1   
(integer) 3 
127.0.0.1:6379> pfmerge k2 k1 program   #将k1和program中的值合并统计到k2中 
OK 
127.0.0.1:6379> pfcount k2 
(integer) 6    # a b c java c++ php

如果允许容错,一定使用HHL

第9节:Bitmaps

9.1、简介

(位存储) 现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图

image.png

【位操作】合理地使用操作位能够有效地提高内存使用率和开发效率。

Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

  • Bitmaps本身不是一种数据类型,实际上它就是字符串(key-value),但是它可以对字符串的位进行操作。
  • Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量

image.png

统计用户信息、活跃状态否、登录、打开等这种两状态量的,都可以使用Bitmaps

9.2、命令

1、setbit

setbit<key><offset><value>:设置Bitmaps中某个偏移量offset(数组下标)的值(0或1)【设置Bitmaps数组对应的下标的值】,偏移量从0开始

实例

将每个独立用户是否访问过网站存放在Bitmaps中,即将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id。

设置键的第offset个位的值(从0算起),假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图

image.png

实现:unique:users:20210101代表2021-01-01这天的独立访问用户的Bitmaps

127.0.0.1:6379> setbit users:20210101 1 1 
(integer) 0 
127.0.0.1:6379> setbit users:20210101 6 1 
(integer) 0 
127.0.0.1:6379> setbit users:20210101 11 1 
(integer) 0 
127.0.0.1:6379> setbit users:20210101 15 1 
(integer) 0 
127.0.0.1:6379> setbit users:20210101 19 1 
(integer) 0

注意:

  • 很多应用的用户id以一个指定数字(例如10000) 开头,直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。
  • 在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞。

2、getbit

getbit<key><offset>获取Bitmaps中某个偏移量的值

127.0.0.1:6379> getbit users:20210101 6 
(integer) 1 
127.0.0.1:6379> getbit users:20210101 5    #没有设置Bitmaps默认就是0
(integer) 0

获取键的第offset位的值(从0开始算)

3、bitcount

统计字符串被设置为1的bit数(打卡天数)

一般情况下,给定的整个字符串都会被进行计数, 通过指定额外的 start 或 end 参数 ,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含。

bitcount<key>[start end] :统计字符串从start字节到end字节比特值为1的数量

实例

计算2021-1-01这天的独立访问用户数量

127.0.0.1:6379> bitcount users:20210101 
(integer) 5

start和end代表起始和结束字节数,下面操作计算用户id在第1个字节【1字节=8位】到第3个字节之间的独立访问用户数, 对应的用户id是11, 15, 19。

127.0.0.1:6379> bitcount users:20210101 1 3 
(integer) 3

注意:redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置。

4、bitop

(1)格式

bitop and(or/not/xor) <destkey> [key…]

bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。

(2)实例

2021-01-01 日访问网站的userid=1,2,5,9。

setbit unique:users:20210101 1 1 
setbit unique:users:20210101 2 1  
setbit unique:users:20210101 5 1  
setbit unique:users:20210101 9 1

2021-01-02 日访问网站的userid=0,1,4,9。

setbit unique:users:20210102 0 1  
setbit unique:users:20210102 1 1  
setbit unique:users:20210102 4 1  
setbit unique:users:20210102 9 1

计算出两天都访问过网站的用户数量(交集)

127.0.0.1:6379> bitop and unique:users:20210101_02 unique:users:20210102 unique:users20210101 
(integer) 2   #因为只有 1和9是重复的

image.png

计算出任意一天都访问过网站的用户数量(例如月活跃就是类似这种),可以使用or求并集

127.0.0.1:6379> bitop or unique:users:20210101@02 unique:users:20210102 unique:users20210101 
(integer) 2   #

9.3、Bitmaps与set对比

假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表

set和Bitmaps存储一天活跃用户对比
数据类型每个用户id占用空间需要存储的用户量全部内存量
集合类型64位5000000064位*50000000 = 400MB
Bitmaps1位1000000001位*100000000 = 12.5MB

很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的

set和Bitmaps存储独立用户空间对比
数据类型一天一个月一年
集合类型400MB12GB144GB
Bitmaps12.5MB375MB4.5GB

但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户),那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0,浪费空间

set和Bitmaps存储一天活跃用户对比(独立用户比较少)
数据类型每个userid占用空间需要存储的用户量全部内存量
集合类型64位10000064位*100000 = 800KB
Bitmaps1位1000000001位*100000000 = 12.5MB