本笔记是学习Redis视频教程整理:https://www.bilibili.com/video/BV1Rv41177Af?spm_id_from=333.337.search-card.all.click
1.Redis概述(来自百度百科)
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助
2. Redis环境搭建
为了在Windows系统中方便使用Redis,我们采用Docker的方式安装Redis
-
Windows系统安装Docker
安装教程可以参考这篇文章Docker安装
-
拉取Redis镜像
docker pull redis:6.2.7
-
启动Redis
-
在本地主机创建挂载目录
因为Docker拉取下的镜像是没有redis.conf文件的,可以从官网redis6.2配置文件下载,放入conf目录中
-
运行容器
docker run -p 16379:6379 --name redis1 -v F:\redis\data:/data -v F:\redis\conf\redis.conf:/etc/redis/redis.conf -d redis:6.2.7 redis-server /etc/redis/redis.conf
-p:指定映射的ip和端口,前面为本地端口,后面为redis端口,这里是指本地访问16379就相当于访问redis的6379端口
-name:容器名称
-v:挂载目录,冒号前是本地目录,冒号后是redis容器内目录
-d:后台运行
redis:6.2.7:redis的镜像版本
redis-server /etc/redis/redis.conf:以配置文件启动redis,加载容器内的conf文件,最终找到的是挂载的目录的redis.conf
-
这样子一个容器就运行起来了,通过Docker的UI工具可以查看到
-
-
测试
-
进入Redis容器进行操作
docker exec -it redis1 /bin/bash
-it:交互式进入容器
redis1:上面我们运行在后台的redis容器
/bin/bash:指定shell
-
使用redis-cli连接redis,并测试连接ping
-
-
Redis关闭
输入命令
shutdown
,则会关闭Redis,以及docker容器也会直接关闭
3.Redis相关知识
-
Redis相关命令
可参考该文档Redis 命令参考
-
Redis相关知识
- 默认端口6379
- 默认16个数据库,类似数组下标,从0开始,初始默认使用0号库
- 统一密码管理,所有库密码相同
redis是单线程+多路IO复用技术
多路复用是指使用一个线程来检测多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞到超时,得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
串行 vs 多线程 + 锁(memcached) vs 单线程 + 多路复用(redis)
redis与memcache三不同:支持多数据类型,支持持久化,单线程+多路复用
在Redis6.0中提供了多线程,命令解析和io数据读写这部分采用了多线程,而命令执行还是采用单线程,多个客户端发送来的命令会在同一个线程去执行,相当于排队执行。
4. Redis的5大数据类型
redis中的数据都是以key/value形式存储的,5大数据类型指的是value的数据类型,key的类型都是字符串。
4.1 Redis的键(key)常用命令
keys *
:查看当前库的所有的keyexists key
:判断某个key是否存在type key
:查看key的类型del key
:删除指定的key数据unlink key
:根据value选择非阻塞删除(仅仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作)dump key
:序列化指定的key,并返回序列化后的值expire key 10
:给key设置有效期10秒,10秒后过期ttl key
:查看指定的key还有多少秒过期, -1:表示永不过期; -2:表示已过期persist key
:表示移出一个key的过期时间,这样该key就变成永不过期了select dbindex
:切换数据库,比如:select 1
,表示切换到1号库dbsize
:查看当前数据库的key的数量flushdb
:清空当前库flushall
:清空全部库
4.2 Redis字符串(String)
-
简介
String是Redis最基本的类型,一个key对应一个value;
String类型是二进制安全的,意味着Redis的String可以包含任何数据,比如jpg图片或者序列化的对象;
String类型最多可以是512M
-
常用命令
-
set <key> <value>
添加键值对set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
- EX:key的过期时间,单位秒
- PX:key的过期时间,单位毫秒,与EX互斥
- NX:当数据库中key不存在时,可以将key-value添加到数据库
- XX:当数据库中key存在时,可以将key-value添加到数据库,与NX互斥
- value中如果包含空格,特殊字符,需要使用双引号包裹
set命令有两条默认规则:
- 当key不存在时,设置新的key/value
- 当key存在时,覆盖原有的key/value
127.0.0.1:6379> set k1 v1 # k1不存在,设置新的key OK 127.0.0.1:6379> get k1 "v1" 127.0.0.1:6379> set k1 v11 # k1 存在,覆盖k1的value OK 127.0.0.1:6379> get k1 "v11"
使用NX参数:只有key不存在时,set才会生效(可以理解为加上这个参数只能新增):
127.0.0.1:6379> set k1 hello nx # k1是存在的,所以set命令不会生效,返回了nil,表示失败 (nil) 127.0.0.1:6379> get k1 # 重新获取值,还是原来的value "v11" 127.0.0.1:6379> set name hello nx # name这个key不存在,所以set命令成功 OK 127.0.0.1:6379> get name "hello"
使用XX参数:只有key存在时,set才会生效(可以理解为加上这个参数只能修改):
127.0.0.1:6379> set name zhangsan xx # name这个key是存在的,所以set可设置 OK 127.0.0.1:6379> get name # value被修改了 "zhangsan" 127.0.0.1:6379> set sex female xx # sex这个key不存在,所以set不生效 (nil) 127.0.0.1:6379> get sex (nil)
-
get <key>
:获取值 -
append <key> <value>
:追加值,将给定的value追加到原始值的末尾。127.0.0.1:6379> set welcome hello OK 127.0.0.1:6379> get welcome "hello" 127.0.0.1:6379> append welcome world (integer) 10 #总长度 127.0.0.1:6379> get welcome "helloworld"
-
strlen <key>
:获取值的长度127.0.0.1:6379> strlen welcome (integer) 10
-
setnx <key> <value>
:只有key不存在时,才设置key的值(这不和set命令带nx参数一样嘛)127.0.0.1:6379> flushdb # 先清空数据库,方便测试 OK 127.0.0.1:6379> setnx name zhangsan # name不存在,返回1,表示设置成功 (integer) 1 127.0.0.1:6379> setnx name lisi # name存在的,所以无法设置,返回0 (integer) 0 127.0.0.1:6379> get name "zhangsan"
-
incr <key>
:原子递增1将key中存储的值增加1,只能对数字值进行操作,如果key不存在,则会新建一个,值为1
127.0.0.1:6379> set age 18 OK 127.0.0.1:6379> incr age # age值加1 (integer) 19 127.0.0.1:6379> get age "19" 127.0.0.1:6379> incr money # money不存在,新建一个key,value为1 (integer) 1 127.0.0.1:6379> get money "1" 127.0.0.1:6379> incr name # name的value不是数字值的,会报错 (error) ERR value is not an integer or out of range
-
decr <key>
:原子递减1将key中存储的值减1,只能对数字值操作,如果为空,新增一个key,value为-1
127.0.0.1:6379> get age "19" 127.0.0.1:6379> decr age # age值减1 (integer) 18 127.0.0.1:6379> get age "18" 127.0.0.1:6379> decr salary # salary不存在,新增一个,value为-1 (integer) -1 127.0.0.1:6379> get salary "-1" 127.0.0.1:6379> decr name # name的value不是数字值的,报错 (error) ERR value is not an integer or out of range
-
incrby/decrby <key> <step>
将key中存储的数字值递增指定的step,若key不存在,则相当于在原值为0的值上递增指定的step,或递减指定的step
127.0.0.1:6379> flushdb # 清空数据库 OK 127.0.0.1:6379> set salary 1000 # 设置数字值的key OK 127.0.0.1:6379> incrby salary 200 # 递增200的step (integer) 1200 127.0.0.1:6379> get salary "1200" 127.0.0.1:6379> decrby salary 500 # 递减500 (integer) 700 127.0.0.1:6379> get salary "700" 127.0.0.1:6379> incrby age 10 # 不存在的key,则初始值为0基础上递增10 (integer) 10 127.0.0.1:6379> get age "10" 127.0.0.1:6379> del age (integer) 1 127.0.0.1:6379> get age (nil) 127.0.0.1:6379> decrby age 5 # 不存在的key,则初始值为0基础上递减少5 (integer) -5 127.0.0.1:6379> get age "-5"
-
mset <key1> <value1> <key2> <value2> ...
: 同时设置多个key-value127.0.0.1:6379> mset name zhangsan age 18 sex male OK 127.0.0.1:6379> get name "zhangsan" 127.0.0.1:6379> get age "18" 127.0.0.1:6379> get sex "male"
-
mget <key1> <key2> ....
:同时获取多个key对应的value127.0.0.1:6379> mget name sex age 1) "zhangsan" 2) "male" 3) "18"
-
msetnx <key1> <value1> <key2> <value2> ...
:同时设置一个或多个key-value,仅当所有要设置的key不存在时生效原子性操作,有一个失败则都失败
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> msetnx name zhangsan age 18 # name,age两个key都不存,设置成功 (integer) 1 127.0.0.1:6379> mget name age 1) "zhangsan" 2) "18" 127.0.0.1:6379> set sex female # 提前设置了sex这个key OK 127.0.0.1:6379> msetnx sex male addr beijing # 同时设置sex和addr,但是sex已经存在了,所以都设置失败,返回0 (integer) 0 127.0.0.1:6379> mget sex addr # 同时获取sex和addr,sex因为最开始设置了为female,所以有值,addr设置失败了,没有这个key,所以为nil 1) "female" 2) (nil)
-
getrange <key> <start> <end>
:获取值的范围,类似java的substring,索引从0开始127.0.0.1:6379> set k1 helloworld OK 127.0.0.1:6379> getrange k1 0 5 # 坐标从0开始的 "hellow" 127.0.0.1:6379> set k2 123456789 OK 127.0.0.1:6379> getrange k2 4 8 "56789"
-
setrange <key> <offset> <value>
:从指定位置开始覆盖指定key的值,索引从0开始算127.0.0.1:6379> set k1 helloworld OK 127.0.0.1:6379> setrange k1 4 Chinese # 从索引4开始覆盖 (integer) 11 127.0.0.1:6379> get k1 "hellChinese"
-
setex <key> <过期时间> <value>
:设置key-value的同时设置过期时间,单位秒(和set命令带ex参数一个意思嘛)127.0.0.1:6379> setex k2 10 v2 # 设置key为k2,value为v2, 过期时间10s OK 127.0.0.1:6379> ttl k2 # 查看过期时间还剩多少时间 (integer) 7 127.0.0.1:6379> ttl k2 (integer) 3 127.0.0.1:6379> ttl k2 (integer) 0 127.0.0.1:6379> ttl k2 # -2表示已过期 (integer) -2
-
getset <key> <value>
:设置新值的同时,返回旧值127.0.0.1:6379> get k1 "hellChinese" 127.0.0.1:6379> getset k1 v1 # 设置新值为v1,返回旧值 "hellChinese" 127.0.0.1:6379> getset k2 v2 # k2不存在,返回旧值为nil (nil) 127.0.0.1:6379> keys * 1) "k2" 2) "k1" 127.0.0.1:6379> get k2 "v2"
-
-
数据结构
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS),是可以修改的字符串,内部结构上类似于Java的ArrayList,采用分配冗余空间的方式来减少内存的频繁分配。
如图所示,内部为当前字符串实际分配的空间为
capacity
,一般要比实际字符串长度len
要高,当字符串长度小于1M时,扩容都是加倍现有的空间;如果超过1M,扩容时一次只会多扩容1M的空间。需要注意的是字符串的最大长度为512M。
4.3 Redis列表List
-
简介
单键多值。
Redis列表就是简单的字符串列表,按照插入顺序排序。
我们可以添加一个元素到列表的头部(左边)或者尾部(右边)。
它的底层实际上是使用双向链表实现的,对两端的操作性能很高,通过索引下标操作中间节点的性能会比较差。
-
常用命令
-
lpush/rpush <key1> <value1> <value2> ....
:从左边或者右边插入一个或多个值127.0.0.1:6379> flushdb OK 127.0.0.1:6379> lpush subject java redis c++ linux # 从左边依次插入value (integer) 4 127.0.0.1:6379> lrange subject 0 -1 # 因为是从左开始插入,每一次都是从左边插入,这样子先插入的就会被挤到右边,所以读出来时从左往右是和插入时候顺序相反的 1) "linux" 2) "c++" 3) "redis" 4) "java" 127.0.0.1:6379> rpush name zhangsan lisi wangwu (integer) 3 127.0.0.1:6379> lrange name 0 -1 # 和从左边插入刚好相反 1) "zhangsan" 2) "lisi" 3) "wangwu"
-
lrange <key> <start> <stop>
:返回列表key中指定区间内的元素,区间以偏移量start和stop指定。参数start和stop都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第一个参数,以此类推;
负数下标,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。
**注意lrang命令获取的区间是闭区间,也就是最后一个元素是包括的, 比如
lrange 0 10
**获取到的是11个元素 -
lpop/rpop <key> <count>
:移除并返回列表key的头部元素或者尾部元素count: 可以省略,默认值为1
lpop/rpop操作后,弹出来的值会在列表中被删除
列表中的值在键在,值光键亡(所有值都没了,键就删除了)
127.0.0.1:6379> lrange subject 0 -1 # subject内有四个value 1) "linux" 2) "c++" 3) "redis" 4) "java" 127.0.0.1:6379> lpop subject # 从左边移除一个,默认count为1,返回移除的值"linux" "linux" 127.0.0.1:6379> lpop subject # 从左边移除一个,默认count为1,返回移除的值"c++" "c++" 127.0.0.1:6379> lrange subject 0 -1 # 目前只剩下2个value了 1) "redis" 2) "java" 127.0.0.1:6379> lpop subject 2 # 从左边移除2个value,count为2 1) "redis" 2) "java" 127.0.0.1:6379> lrange subject 0 -1 # value都没了 (empty array) 127.0.0.1:6379> keys * # 对应的key,subject也被删除了 1) "name"
127.0.0.1:6379> lrange name 0 -1 # 与上面同样的操作,只是换成rpop 1) "zhangsan" 2) "lisi" 3) "wangwu" 127.0.0.1:6379> rpop name "wangwu" 127.0.0.1:6379> rpop name 2 1) "lisi" 2) "zhangsan" 127.0.0.1:6379> keys * (empty array)
-
rpoplpush source destination
:从一个列表的右边弹出一个元素放到另一个列表如果source不存在,则返回nil,不执行其他动作
如果source和destination相同,则列表中国表尾数据被移动到表头,并返回该元素,这种特殊情况视作列表的旋转操作。
127.0.0.1:6379> rpush k1 1 2 3 4 # 新增列表k1 (integer) 4 127.0.0.1:6379> lrange k1 0 -1 1) "1" 2) "2" 3) "3" 4) "4" 127.0.0.1:6379> rpush k2 5 6 7 # 新增列表k2 (integer) 3 127.0.0.1:6379> lrange k2 0 -1 1) "5" 2) "6" 3) "7" 127.0.0.1:6379> rpoplpush k1 k2 # 列表k1的尾部元素弹出到k2的头部,返回这个元素"4" "4" 127.0.0.1:6379> lrange k1 0 -1 1) "1" 2) "2" 3) "3" 127.0.0.1:6379> lrange k2 0 -1 1) "4" 2) "5" 3) "6" 4) "7" 127.0.0.1:6379> rpoplpush k2 k2 # source和dest一样,相当于旋转操作 "7" 127.0.0.1:6379> lrange k2 0 -1 1) "7" 2) "4" 3) "5" 4) "6"
-
lindex key index
:获取指定索引位置的元素(从左到右)下标(index)参数
start
和stop
都以0
为底,也就是说,以0
表示列表的第一个元素,以1
表示列表的第二个元素,以此类推。也可以使用负数下标,以
-1
表示列表的最后一个元素,-2
表示列表的倒数第二个元素,以此类推。如果
key
不是列表类型,返回一个错误127.0.0.1:6379> flushdb OK 127.0.0.1:6379> rpush subject java linux c++ php (integer) 4 127.0.0.1:6379> lindex subject 2 # 获取索引位置2的元素 "c++" 127.0.0.1:6379> lindex subject 10 # 获取索引位置10的元素,没有这个值 (nil) 127.0.0.1:6379> lindex subject -1 # 获取列表最后一个元素 "php"
-
llen key
:获取列表长度如果key不存在,则key别解释为一个空列表,返回0
如果key不是列表类型,返回一个错误
127.0.0.1:6379> llen subject (integer) 4
-
linsert <key> before|after <value> <newvalue>
:在某个值的前面或后面插入一个值当value不存在于列表key时,不执行任何操作。
当key不存在时,key被视为空列表,不执行任何操作。
如果key不是列表类型,返回一个错误。
返回值:
- 如果命令执行成功,返回插入操作完成之后,列表的长度
- 如果没有找到value,返回-1
- 如果key不存在或为空列表,返回0
127.0.0.1:6379> lrange subject 0 -1 # 列表原始数据 1) "java" 2) "linux" 3) "c++" 4) "php" 127.0.0.1:6379> linsert subject before linux c# # 在subject中的linux前插入一个c#,返回长度5 (integer) 5 127.0.0.1:6379> lrange subject 0 -1 # 查看插入成功 1) "java" 2) "c#" 3) "linux" 4) "c++" 5) "php" 127.0.0.1:6379> linsert subject before abc redis # 在一个不存在的value之前插入数据,返回-1 (integer) -1 127.0.0.1:6379> linsert subject1 before java redis # 在一个不存在的列表中插入,返回0 (integer) 0 127.0.0.1:6379> linsert subject after c++ redis # 在subject中的c++之后插入一个redis,返回长度6 (integer) 6 127.0.0.1:6379> lrange subject 0 -1 # 查看插入成功 1) "java" 2) "c#" 3) "linux" 4) "c++" 5) "redis" 6) "php"
-
lrem key count value
: 根据参数count的值,移除列表中与参数value相同的元素count的值可以是以下几种:
- count > 0:从表头开始向表尾搜索,移除与value相等的元素,数量为count
- count < 0:从表尾开始向表头搜索,移除与value相等的元素,数量为count的绝对值
- count = 0:移除表中所有与value相等的值
返回值:
被移除元素的数量。
因为不存在的
key
被视作空表(empty list),所以当key
不存在时, lrem命令总是返回0
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> rpush char a b c a d a e a f g #新建列表 (integer) 10 127.0.0.1:6379> lrem char 2 a # count = 2 > 0,从表头开始向表尾搜索,移除2个a (integer) 2 127.0.0.1:6379> lrange char 0 -1 # 查看当前列表,前面两个a被移除 1) "b" 2) "c" 3) "d" 4) "a" 5) "e" 6) "a" 7) "f" 8) "g" 127.0.0.1:6379> lrem char -1 a # count = -1 < 0,从表尾开始向表头搜索,移除1个a (integer) 1 127.0.0.1:6379> lrange char 0 -1 # 查看当前列表,后面的一个a被移除 1) "b" 2) "c" 3) "d" 4) "a" 5) "e" 6) "f" 7) "g" 127.0.0.1:6379> rpush char a a a a # 在表尾重新加入4个a (integer) 11 127.0.0.1:6379> lrange char 0 -1 1) "b" 2) "c" 3) "d" 4) "a" 5) "e" 6) "f" 7) "g" 8) "a" 9) "a" 10) "a" 11) "a" 127.0.0.1:6379> lrem char 0 a # count = 0,移除列表所有的a (integer) 5 127.0.0.1:6379> lrange char 0 -1 1) "b" 2) "c" 3) "d" 4) "e" 5) "f" 6) "g"
-
lset <key> <index> <value>
:替换指定位置的值当index参数超出范围,或对一个空列表(key不存在)进行
lset
时,返回一个错误127.0.0.1:6379> keys * 1) "char" 127.0.0.1:6379> type char list 127.0.0.1:6379> lrange char 0 -1 1) "b" 2) "c" 3) "d" 4) "e" 5) "f" 6) "g" 127.0.0.1:6379> lset char 2 change # 将index为2的元素替换为change OK 127.0.0.1:6379> lrange char 0 -1 # 查看list,替换成功 1) "b" 2) "c" 3) "change" 4) "e" 5) "f" 6) "g" 127.0.0.1:6379> lset char 20 hello # 参数index超出范围,报错 (error) ERR index out of range 127.0.0.1:6379> lset test 0 hello # key不存在,报错 (error) ERR no such key
-
-
数据结构
List的数据结构为快速链表quickList
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构就是ziplist,也就是压缩列表(7.0版本使用listpack替代了)
它将所有的元素紧挨着一起存储,分配的是一块连续的内存,当数据量较多的时候才会改成quicklist
因为普通的链表需要的附加指针空间太大,会比较浪费空间,比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
Redis将链表和ziplist结合起来组成了quicklist,也就是将多个ziplist使用双向指针串起来使用,这样既满足了快速插入删除性能,又不会出现太大的空间冗余。
4.4 Redis集合(Set)
-
简介
Redis的集合set对外提供的功能与list类似,是一个列表的功能,特殊之处在于set是可以去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择。
set集合还提供了判断某个成员是否存在一个set集合内的重要接口,这个是list所不能提供的。
Redis的Set是string类型的无序集合,它的底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)
一个算法,如果时间复杂度是O(1),那么随着数据的增加,查找数据的时间不变,也就是不管数据多少,查找时间都是一样的。
-
常用命令
-
sadd <key> <value1> <value2> ....
:添加一个或多个元素到key集合中,已存在的元素会被忽略127.0.0.1:6379> flushdb OK 127.0.0.1:6379> sadd name zhangsan lisi wangwu lisi # 添加4个元素,只有三个成功添加,另一个重复的自动去除 (integer) 3
-
smembers <key>
:取出集合的所有值127.0.0.1:6379> smembers name 1) "wangwu" 2) "lisi" 3) "zhangsan"
-
sismember <key> <value>
:判断集合中是否包含某个值如果
value
元素是集合的成员,返回1
。如果
value
元素不是集合的成员,或key
不存在,返回0
127.0.0.1:6379> sismember name lisi (integer) 1 127.0.0.1:6379> sismember name lucy # value不是集合的成员 (integer) 0
-
scard <key>
:获取集合中元素的个数,集合不存在返回0127.0.0.1:6379> scard name (integer) 3 127.0.0.1:6379> scard person # 集合不存在 (integer) 0
-
srem key member [member ....]
:移除集合key中的一个或多个member元素,不存在的member元素会被忽略当key不是集合类型时,返回一个错误。
返回值:被成功移除的元素的数量,不包括被忽略的元素
127.0.0.1:6379> sadd subject java c++ python php (integer) 4 127.0.0.1:6379> srem subject c++ php linux # linux不存在被忽略 (integer) 2 127.0.0.1:6379> smembers subject # 移除后剩下的 1) "python" 2) "java"
-
spop <key> <count>
:随机移除并返回集合中count个元素count默认值为1
127.0.0.1:6379> sadd subject linux go mysql (integer) 3 127.0.0.1:6379> smembers subject 1) "linux" 2) "python" 3) "java" 4) "go" 5) "mysql" 127.0.0.1:6379> spop subject # 随机移除1个元素并返回 "go" 127.0.0.1:6379> spop subject 2 # 随机移除2个元素并返回 1) "mysql" 2) "java" 127.0.0.1:6379> smembers subject 1) "python" 2) "linux"
-
srandmember <key> <count>
:随机从集合中获取多个元素,不会从集合中删除count默认值是1,可以不指定;
srandmember和spop的区别是:spop会删除集合中该元素,而srandmember不会删除
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> sadd subject java c++ linux python php redis (integer) 6 127.0.0.1:6379> smembers subject 1) "java" 2) "redis" 3) "php" 4) "c++" 5) "linux" 6) "python" 127.0.0.1:6379> srandmember subject # 随机获取一个元素 "python" 127.0.0.1:6379> smembers subject # 输出所有元素,没有被删除 1) "redis" 2) "php" 3) "c++" 4) "java" 5) "linux" 6) "python" 127.0.0.1:6379> srandmember subject 3 # 随机获取3个元素 1) "php" 2) "c++" 3) "java" 127.0.0.1:6379> smembers subject # 集合元素没有变化 1) "redis" 2) "php" 3) "c++" 4) "java" 5) "linux" 6) "python"
-
smove <source> <destination> member
:将集合的某个元素移动到另一个集合-
smove
是原子性操作; -
如果source集合不存在或不包含member元素,则smove命令不执行任何操作,仅返回0;否则,member元素从source集合中移除,添加到destination集合中;
-
当destination集合中已经包含了member元素,smove命令只是简单的移除source集合中的member元素;
-
当source或destination不是集合类型时,返回一个错误
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> sadd subject java linux c++ # 新增一个集合 (integer) 3 127.0.0.1:6379> sadd name zhangsan lisi wangwu # 新增一个集合 (integer) 3 127.0.0.1:6379> smove subject name linux # 将集合subject中的linux元素移动到集合name (integer) 1 127.0.0.1:6379> smembers subject # 查看集合元素,linux元素被移除 1) "c++" 2) "java" 127.0.0.1:6379> smembers name # 查看集合元素,新增了linux元素 1) "linux" 2) "wangwu" 3) "lisi" 4) "zhangsan" 127.0.0.1:6379> sadd subject zhangsan # 集合subject新增一个元素zhangsan (integer) 1 127.0.0.1:6379> smove subject name zhangsan # 将zhangsan移动到name集合 (integer) 1 127.0.0.1:6379> smembers subject # name集合已有zhangsan元素,只移除subject中的元素 1) "c++" 2) "java" 127.0.0.1:6379> smembers name 1) "linux" 2) "wangwu" 3) "lisi" 4) "zhangsan"
-
-
sinter key [key ...]
:取多个集合的交集127.0.0.1:6379> flushdb OK 127.0.0.1:6379> sadd subject java linux c++ (integer) 3 127.0.0.1:6379> sadd subject1 java c++ redis (integer) 3 127.0.0.1:6379> sinter subject subject1 1) "c++" 2) "java"
-
sinterstore destination key [key ....]
:将多个集合的交集放到一个新的集合中- 这个命令与
sinter
类似,后面多了个store,就是将多个集合的交集的结果保存到destination集合中; - 如果destination集合已存在,则会将其覆盖
- destination也可以是key本身。
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> sadd subject java linux c++ (integer) 3 127.0.0.1:6379> sadd subject1 java c++ mysql (integer) 3 127.0.0.1:6379> sinterstore newSubject subject subject1 # 取两个集合的交集保存到newSubject (integer) 2 127.0.0.1:6379> smembers subject # 查看原集合,没有变化 1) "c++" 2) "linux" 3) "java" 127.0.0.1:6379> smembers subject1 # 查看原集合,没有变化 1) "c++" 2) "java" 3) "mysql" 127.0.0.1:6379> smembers newSubject # 查看新集合 1) "c++" 2) "java" 127.0.0.1:6379> sinterstore subject subject subject1 # 新集合为key本身 (integer) 2 127.0.0.1:6379> smembers subject # subject集合被覆盖为交集的结果 1) "c++" 2) "java"
- 这个命令与
-
sunion key [key ...]
:取多个集合的并集,并自动去重127.0.0.1:6379> keys * 1) "subject1" 2) "newSubject" 3) "subject" 127.0.0.1:6379> smembers subject 1) "c++" 2) "java" 127.0.0.1:6379> smembers subject1 1) "c++" 2) "java" 3) "mysql" 127.0.0.1:6379> smembers newSubject 1) "c++" 2) "java" 127.0.0.1:6379> sunion subject subject1 newSubject # 返回3个集合的并集,并自动去重 1) "c++" 2) "java" 3) "mysql"
-
sunionstore destination key [key...]
:获取多个集合的并集放到一个新的集合中- 这个命令与
sunion
类似,后面多了个store,就是将多个集合的并集的结果保存到destination集合中; - 如果destination集合已存在,则会将其覆盖
- destination也可以是key本身。
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> sadd subject java c++ linux mysql (integer) 4 127.0.0.1:6379> sadd subject1 c++ python redis ubuntu (integer) 4 127.0.0.1:6379> sunionstore newSubject subject subject1 (integer) 7 127.0.0.1:6379> smembers newSubject # 两个集合的并集存放到newSubject,并去重 1) "redis" 2) "c++" 3) "java" 4) "mysql" 5) "python" 6) "linux" 7) "ubuntu" 127.0.0.1:6379> sunionstore subject subject subject1 (integer) 7 127.0.0.1:6379> smembers subject # 将并集结果放到了subject,覆盖了subject 1) "redis" 2) "c++" 3) "java" 4) "mysql" 5) "python" 6) "linux" 7) "ubuntu"
- 这个命令与
-
sdiff key1 [key2 ...]
:取多个集合的差集(key1中有的,key2中没有的)127.0.0.1:6379> sadd subject java c++ python (integer) 3 127.0.0.1:6379> sadd subject1 java redis mysql (integer) 3 127.0.0.1:6379> sdiff subject subject1 1) "c++" 2) "python"
-
sdiffstore destination key [key ...]
:将多个集合的差集放到一个新的集合中- 这个命令与
sdiff
类似,后面多了个store,就是将多个集合的差集的结果保存到destination集合中; - 如果destination集合已存在,则会将其覆盖
- destination也可以是key本身。
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> sadd subject java c++ python (integer) 3 127.0.0.1:6379> sadd subject1 java redis mysql (integer) 3 127.0.0.1:6379> sdiffstore newSubject subject subject1 (integer) 2 127.0.0.1:6379> smembers newSubject 1) "c++" 2) "python"
- 这个命令与
-
-
数据结构
set的数据结构是字典,字典是用hash表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。
Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
4.5. Redis哈希(Hash)
-
简介
Redis hash是一个键值对集合;
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象,类似java里面的
Map<String,Object>
-
常用命令
-
hset key field value [field value ...]
:设置多个field的值如果key不存在,一个新的哈希表被创建并进行hset操作;
如果域field已经存在哈希表中,则旧值会被覆盖
127.0.0.1:6379> hset user name zhangsan age 18 # 哈希表user设置2个域,name和age,值分别为zhangsan和18 (integer) 2 127.0.0.1:6379> hget user name # 获取指定的field "zhangsan" 127.0.0.1:6379> hget user age "18"
-
hget key field
: 获取哈希表中指定的field -
hgetall key
:获取哈希表中所有的域和值127.0.0.1:6379> hgetall user 1) "name" 2) "zhangsan" 3) "age" 4) "18"
-
hexists key field
:判断给定的field是否存在哈希表中域存在,返回1;
域不存在或者key不存在,返回0
127.0.0.1:6379> hexists user name # name这个field存在,返回1 (integer) 1 127.0.0.1:6379> hexists user sex # sex这个域不存在,返回0 (integer) 0 127.0.0.1:6379> hexists user1 name # user1不存在 (integer) 0
-
hkeys key
:列出哈希表所有的filed127.0.0.1:6379> hkeys user 1) "name" 2) "age"
-
hvals key
:列出哈希表所有的value127.0.0.1:6379> hvals user 1) "zhangsan" 2) "18"
-
hlen key
:返回哈希表中域的数量127.0.0.1:6379> hlen user (integer) 2
-
hincrby key field increment
:field的值加上指定的增量- 增量也可以为负数,相当于对给定域进行减法操作;
- 如果key不存在,一个新的哈希表被创建并执行hincrby命令;
- 如果field不存在,那么在执行命令前,域的值被初始化为0;
- 对一个存储字符串值的域执行这个命令会造成一个错误。
- 本操作的值被限制在64位(bit)有符号数字表示之内
127.0.0.1:6379> hset user salary 1000 # 哈希表添加一个域salary (integer) 1 127.0.0.1:6379> hgetall user # 当前哈希表的所有域和值 1) "name" 2) "zhangsan" 3) "age" 4) "18" 5) "salary" 6) "1000" 127.0.0.1:6379> hincrby user salary 200 # salary域增加200 (integer) 1200 127.0.0.1:6379> hget user salary # salary域当前值 "1200" 127.0.0.1:6379> hincrby user salary -500 # salary域增加-500,相当于减去500 (integer) 700 127.0.0.1:6379> hget user salary "700" 127.0.0.1:6379> hincrby user name -10 # 非数字类型的值报错 (error) ERR hash value is not an integer 127.0.0.1:6379> hincrby user timestamp 100 # timestamp域不存在,默认添加域,初始化为0,相当于0+100 (integer) 100 127.0.0.1:6379> hget user timestamp "100"
-
hsetnx key field value
:当域不存在的时候,添加域和值如果field已经存在了,这个操作无效;
如果key不存在,一个新的哈希表被创建,并且执行这个hsetnx命令
127.0.0.1:6379> hkeys user 1) "name" 2) "age" 3) "salary" 4) "timestamp" 127.0.0.1:6379> hsetnx user name lisi # name这个域存在,所以设置无效 (integer) 0 127.0.0.1:6379> hsetnx user addr beijing # addr这个域不存在,所以设置生效 (integer) 1 127.0.0.1:6379> hgetall user 1) "name" 2) "zhangsan" 3) "age" 4) "18" 5) "salary" 6) "700" 7) "timestamp" 8) "100" 9) "addr" 10) "beijing"
-
-
数据结构
Hash类型对应的数据结构是2种:ziplist(压缩列表),hashtable(哈希表)
当field-value长度较短个数较少时,使用ziplist,否则使用hashtable
4.6 Redis有序集合zset(sorted set)
-
简介
redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是:有序集合的每个成员都关联了一个评分(score),这个score被用来按照从最低分到最高分的方式排序集合中的成员。
集合的成员是唯一的,但是score是可以重复的
因为元素是有序的,所以可以很快根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合中的中间元素也是非常快的,因为能够使用有序集合作为一个没有重复成员的智能列表。
-
常用命令
-
zadd <key> <score1> <member1> <score2> <member2> ...
: 添加一个或多个元素及其score值到有序集合key当中- 如果某个member已经是有序集的成员,那么更新这个member的score值,并通过重新插入这个member元素,来保证该member在正确的位置上。
- score值可以是整数或双精度浮点数。
- 如果key不存在,则会创建一个空的有序集并执行zadd操作。
- 当key存在,但不是有序集合类型时,返回一个错误。
127.0.0.1:6379> zadd ranking 100 java 90 c 60 c++ 30 redis 10 php (integer) 5 127.0.0.1:6379> zrange ranking 0 -1 1) "php" 2) "redis" 3) "c++" 4) "c" 5) "java"
-
zrange key start stop [WITHSCORES]
:返回有序集key
中,指定区间内的成员成员的位置按score值递增(从小到大)来排序,如果得分相同,就按字典排序;
下标参数
start
和stop
都以0
为底,也就是说,以0
表示有序集第一个成员,以1
表示有序集第二个成员,以此类推。也可以使用负数下标,以
-1
表示最后一个成员,-2
表示倒数第二个成员,以此类推。带上WITHSCORES选项,可以让成员和score值一起返回
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> zadd ranking 100 java 40 php 70 c 70 redis 20 c++ (integer) 5 127.0.0.1:6379> zrange ranking 0 -1 # 查看所有成员,按score从小到大排列,c和redis的score相同,按字典序,c在r之前 1) "c++" 2) "php" 3) "c" 4) "redis" 5) "java" 127.0.0.1:6379> zrange ranking 0 -1 withscores # 带上withscores,同时返回成员和score 1) "c++" 2) "20" 3) "php" 4) "40" 5) "c" 6) "70" 7) "redis" 8) "70" 9) "java" 10) "100"
-
zrevrange key start stop [WITHSCORES]
:返回有序集key
中,指定区间内的成员(score降序排列)这个命令和上一个命令
zrange
类似,只是返回时以score值降序(从大到小)排列127.0.0.1:6379> keys * 1) "ranking" 127.0.0.1:6379> zrange ranking 0 -1 withscores 1) "c++" 2) "20" 3) "php" 4) "40" 5) "c" 6) "70" 7) "redis" 8) "70" 9) "java" 10) "100" 127.0.0.1:6379> zrevrange ranking 0 -1 withscores 1) "java" 2) "100" 3) "redis" 4) "70" 5) "c" 6) "70" 7) "php" 8) "40" 9) "c++" 10) "20"
-
zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
:返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列,score相同的按字典序排列;
可选的
LIMIT
参数指定返回结果的数量及区间(就像SQL中的SELECT LIMIT offset, count
),注意当offset
很大时,定位offset
的操作可能需要遍历整个有序集,此过程最坏复杂度为 O(N) 时间withscores:可以让成员和score值一起返回
默认情况下,区间的取值使用闭区间 (小于等于或大于等于),你也可以通过给参数前增加
(
符号来使用可选的开区间 (小于或大于)比如:
ZRANGEBYSCORE zset (1 5
返回所有符合条件1 < score <= 5
的成员127.0.0.1:6379> zrange ranking 0 -1 withscores # 首先查看有序集合成员排列 1) "c++" 2) "20" 3) "php" 4) "40" 5) "c" 6) "70" 7) "redis" 8) "70" 9) "java" 10) "100" 127.0.0.1:6379> zrangebyscore ranking 40 70 withscores # 从中获取score在40~70之间的成员 1) "php" 2) "40" 3) "c" 4) "70" 5) "redis" 6) "70" 127.0.0.1:6379> zrangebyscore ranking 40 70 withscores limit 0 2 # 从中获取score在40~70之间的成员,从第0个开始,获取2个 1) "php" 2) "40" 3) "c" 4) "70" 127.0.0.1:6379> zrangebyscore ranking 40 70 withscores limit 1 2 # 从中获取score在40~70之间的成员,从第1个开始,获取2个 1) "c" 2) "70" 3) "redis" 4) "70"
-
zrevrangebyscore key max min [WITHSCORES] [LIMIT offset count]
:返回有序集key
中,score
值介于max
和min
之间(默认包括等于max
或min
)的所有的成员有序集成员按
score
值递减(从大到小)的次序排列。其他与上一条命令
zrangebyscore
一样127.0.0.1:6379> zrange ranking 0 -1 withscores 1) "c++" 2) "20" 3) "php" 4) "40" 5) "c" 6) "70" 7) "redis" 8) "70" 9) "java" 10) "100" 127.0.0.1:6379> zrevrangebyscore ranking 100 70 withscores 1) "java" 2) "100" 3) "redis" 4) "70" 5) "c" 6) "70" 127.0.0.1:6379> zrevrangebyscore ranking 100 70 withscores limit 0 2 1) "java" 2) "100" 3) "redis" 4) "70" 127.0.0.1:6379> zrevrangebyscore ranking 100 70 withscores limit 1 2 1) "redis" 2) "70" 3) "c" 4) "70"
-
zincrby key increment member
:为有序集key
的成员member
的score
值加上增量increment
可以通过传递一个负数值
increment
,让score
减去相应的值,比如ZINCRBY key -5 member
,就是让member
的score
值减去5
。当
key
不存在,或member
不是key
的成员时,ZINCRBY key increment member
等同于ZADD key increment member
。当
key
不是有序集类型时,返回一个错误。score
值可以是整数值或双精度浮点数。127.0.0.1:6379> zrange ranking 0 -1 withscores # 首先查看原始成员的score 1) "c++" 2) "20" 3) "php" 4) "40" 5) "c" 6) "70" 7) "redis" 8) "70" 9) "java" 10) "100" 127.0.0.1:6379> zincrby ranking 10 redis # 将redis的score增加10 "80" 127.0.0.1:6379> zrange ranking 0 -1 withscores # 可以看到redis的score变为了80 1) "c++" 2) "20" 3) "php" 4) "40" 5) "c" 6) "70" 7) "redis" 8) "80" 9) "java" 10) "100" 127.0.0.1:6379> zincrby ranking -10 c # 将c的score增加-10,相当于减去10 "60" 127.0.0.1:6379> zrange ranking 0 -1 withscores # 可以看到c的score变为了60 1) "c++" 2) "20" 3) "php" 4) "40" 5) "c" 6) "60" 7) "redis" 8) "80" 9) "java" 10) "100"
-
zrem key member [member ...]
:移除有序集key
中的一个或多个成员,不存在的成员将被忽略127.0.0.1:6379> zrange ranking 0 -1 1) "c++" 2) "php" 3) "c" 4) "redis" 5) "java" 127.0.0.1:6379> zrem ranking c php # 移除c 和 php (integer) 2 127.0.0.1:6379> zrange ranking 0 -1 1) "c++" 2) "redis" 3) "java"
-
zremrangebyrank key start stop
:移除有序集key
中,指定排名(rank)区间内的所有成员。区间分别以下标参数
start
和stop
指出,包含start
和stop
在内。下标参数
start
和stop
都以0
为底,也就是说,以0
表示有序集第一个成员,以1
表示有序集第二个成员,以此类推。你也可以使用负数下标,以
-1
表示最后一个成员,-2
表示倒数第二个成员,以此类推127.0.0.1:6379> zrange ranking 0 -1 withscores 1) "c++" 2) "20" 3) "redis" 4) "80" 5) "java" 6) "100" 127.0.0.1:6379> zremrangebyrank ranking 0 1 (integer) 2 127.0.0.1:6379> zrange ranking 0 -1 withscores 1) "java" 2) "100"
-
zremrangebyscore key min max
:移除有序集key
中,所有score
值介于min
和max
之间(包括等于min
或max
)的成员score
值等于min
或max
的成员也可以不包括在内,可以往上看命令zrangebyscore
的说明127.0.0.1:6379> flushdb OK 127.0.0.1:6379> zadd ranking 100 java 80 php 50 go 70 c++ (integer) 4 127.0.0.1:6379> zrange ranking 0 -1 withscores 1) "go" 2) "50" 3) "c++" 4) "70" 5) "php" 6) "80" 7) "java" 8) "100" 127.0.0.1:6379> zremrangebyscore ranking 50 70 # 移除score在50 ~ 70之间的成员 (integer) 2 127.0.0.1:6379> zrange ranking 0 -1 withscores 1) "php" 2) "80" 3) "java" 4) "100"
-
zcount key min max
:返回有序集key
中,score
值在min
和max
之间(默认包括score
值等于min
或max
)的成员的数量。关于参数
min
和max
的详细使用方法,与之前一样,仍旧可以使用(
进行开区间127.0.0.1:6379> flushdb OK 127.0.0.1:6379> zadd salary 2000 jack 3500 lucy 4000 mary 1000 tom (integer) 4 127.0.0.1:6379> zrange salary 0 -1 withscores 1) "tom" 2) "1000" 3) "jack" 4) "2000" 5) "lucy" 6) "3500" 7) "mary" 8) "4000" 127.0.0.1:6379> zcount salary 1000 3000 (integer) 2 127.0.0.1:6379> zcount salary 3000 5000 (integer) 2 127.0.0.1:6379> zcount salary (1000 3000 # 不包含1000,所以只有1个 (integer) 1 127.0.0.1:6379> zcount salary (1000 (3500 # 不包含1000,也不包含3500 (integer) 1
-
zrank key member
:返回有序集key
中成员member
的排名。其中有序集成员按score
值递增(从小到大)顺序排列。排名以
0
为底,也就是说,score
值最小的成员排名为0
。127.0.0.1:6379> zrange salary 0 -1 1) "tom" 2) "jack" 3) "lucy" 4) "mary" 127.0.0.1:6379> zrank salary jack (integer) 1 127.0.0.1:6379> zrank salary mary (integer) 3
-
zrevrank key member
:返回有序集key
中成员member
的排名。其中有序集成员按score
值递减(从大到小)排序排名以
0
为底,也就是说,score
值最大的成员排名为0
。127.0.0.1:6379> zrange salary 0 -1 # 从小到大排列 1) "tom" 2) "jack" 3) "lucy" 4) "mary" 127.0.0.1:6379> zrevrange salary 0 -1 # 从大到小排列 1) "mary" 2) "lucy" 3) "jack" 4) "tom" 127.0.0.1:6379> zrevrank salary mary # 按从大到小排列,Mary在第0位 (integer) 0 127.0.0.1:6379> zrevrank salary jack # jack在第2位 (integer) 2
-
zscore key member
:返回有序集key
中,成员member
的score
值。如果
member
元素不是有序集key
的成员,或key
不存在,返回nil
127.0.0.1:6379> zrange salary 0 -1 withscores 1) "tom" 2) "1000" 3) "jack" 4) "2000" 5) "lucy" 6) "3500" 7) "mary" 8) "4000" 127.0.0.1:6379> zscore salary mary "4000" 127.0.0.1:6379> zscore salary tom "1000"
-
-
数据结构
SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名词,还可以通过score的范围来获取元素的列表
zset底层使用了两个数据结构:
-
hash表
hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
-
跳跃表(skiplist)
跳跃表的目的在于给元素排序,根据score的范围获取元素列表。
跳跃表是一个非常优秀的数据结构,实现简单,插入,删除,查找的复杂度均为
O(logN)
类似Java中的
ConcurrentSkipListSet
,根据score的值排序后生成的一个跳表,可以快速按照位置的顺序或者score的顺序查询元素。可以学习下这篇文章跳跃表基本原理和Java实现
-
5. Redis的新的3种数据类型
5.1 Bitmaps
-
简介
现代计算机使用二进制(位)作为信息的基本单位,1个字节等于8位,例如"abc"字符串是由3个字节组成,但实际在计算机内存存储时将其使用二进制表示,"abc"分别对应的ASCII码是:97,98,99,对应的二进制分别是
01100001
,01100010
,01100011
,如下图合理地利用位操作能够有效地提高内存使用率和开发效率。
Redis提供了
Bitmaps
这个"数据类型",可以实现对位的操作-
Bitmaps本身不是一种数据类型,实际上它就是字符串,但是它可以对字符串的位进行操作,字符串中每个字符对应一个字节,也就是8位,一个字符可以存储8个bit的信息。
-
Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同,可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的小标在Bitmaps中叫做偏移量。
-
-
常用命令
-
setbit key offset value
:设置Bitmaps中某个偏移量的值(0或1)offset是从0开始的,offset的值必须大于或等于0,小于2^32(bit映射被限制在512M之内)
value的值只能是0或1
127.0.0.1:6379> setbit bit 10 1 (integer) 0 # bit 默认被初始化为0 127.0.0.1:6379> getbit bit 10 (integer) 1
-
getbit key offset
:获取Bitmaps中offset偏移的值返回0或1
-
示例
记录每个独立用户是否访问过网站,存放在Bitmaps中,访问过的用户记为1,没有访问过的用户记为0,用户id作为offset。
假设现在有20个用户,userid=1,6,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图:
users:20220803
这个Bitmaps表示2022-08-03这条独立访问的用户,如下:127.0.0.1:6379> setbit users:20220803 1 1 (integer) 0 127.0.0.1:6379> setbit users:20220803 6 1 (integer) 0 127.0.0.1:6379> setbit users:20220803 11 1 (integer) 0 127.0.0.1:6379> setbit users:20220803 15 1 (integer) 0 127.0.0.1:6379> setbit users:20220803 19 1 (integer) 0
很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。
在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞
-
bitcount key [start] [end]
:统计Bitmaps中bit位为1的数量- 一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含
- bitcount计算的是byte的位置,start,end是指字节的下标数,所以[a,b]对应的bit范围为[8a,8b+7],具体看下面例子
以上一个例子的数据Bitmaps,
users:20220803
,有5个bit是1127.0.0.1:6379> bitcount users:20220803 (integer) 5
指定start和end参数的时候是指字节,比如计算用户id在第1个字节和第3个字节之间的访问用户数,start=1,end=3,对应的bit范围就是[8,31],所以对应的用户id就是11,15,19
127.0.0.1:6379> bitcount users:20220803 1 3 (integer) 3
-
bitop operation destkey key [key ...]
:对一个或多个bitmap进行位操作-
位操作结果保存在
destkey
中 -
operation
可以是:AND
,OR
,NOT
,XOR
四种任意一种AND
:逻辑与OR
:逻辑或NOT
:逻辑非XOR
:异或 -
除了
NOT
操作之外,其他操作都可以接收一个或多个key
127.0.0.1:6379> flushdb OK 127.0.0.1:6379> setbit bit1 0 1 # 设置bit1的第0位为1 (integer) 0 127.0.0.1:6379> setbit bit1 2 1 # 设置bit1的第2位为1,最终bit1的值为:0101 (integer) 0 127.0.0.1:6379> setbit bit2 1 1 # 设置bit2的第1位为1 (integer) 0 127.0.0.1:6379> setbit bit2 3 1 # 设置bit2的第3位为1,最终bit2的值为:1010 (integer) 0 127.0.0.1:6379> bitop and result bit1 bit2 # bit1与bit2逻辑或,只有1&1才会是1,所以刚好0101&1010=0000 (integer) 1 127.0.0.1:6379> getbit result 0 # 依次读出result的每一bit,都是0 (integer) 0 127.0.0.1:6379> getbit result 1 (integer) 0 127.0.0.1:6379> getbit result 2 (integer) 0 127.0.0.1:6379> getbit result 3 (integer) 0 127.0.0.1:6379> bitop or result bit1 bit2 # bit1和bit2逻辑或,只有0|0才会是0,所以刚好0101|1010=1111 (integer) 1 127.0.0.1:6379> getbit result 0 (integer) 1 127.0.0.1:6379> getbit result 1 (integer) 1 127.0.0.1:6379> getbit result 2 (integer) 1 127.0.0.1:6379> getbit result 3 (integer) 1
-
-
-
Bitmaps和set的比较
假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表
**set和Bitmaps存储一天活跃用户对比 **
数据类型 每个用户id占用空间 需要存储的用户量 全部内存量 set 64位 50000000 64位*50000000 = 400MB Bitmaps 1位 100000000 1位*100000000 = 12.5MB 很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的
set和Bitmaps存储独立用户空间对比
数据类型 一天 一个月 一年 set 400MB 12GB 144GB Bitmaps 12.5MB 375MB 4.5GB 但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。
set和Bitmaps存储一天活跃用户对比(独立用户比较少)
数据类型 每个userid占用空间 需要存储的用户量 全部内存量 set 64位 100000 64位*100000 = 800KB Bitmaps 1位 100000000 1位*100000000 = 12.5MB
5.2 HyperLogLog
-
简介
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr,incrby轻松实现,但是UV(UniqueVisitor独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
- 数据存储在MySQL中,使用distinct count计算不重复个数
- 使用redis提供的hash,set,bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
能否降低一定精度来平衡存储空间?Redis推出了HyperLogLog
Redis的HyperLogLog是用来做基数统计的泛,HyperLogLog的优点是:在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的,并且是很小的。
在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数,这和计算基数时,元素越多耗费内存越多的集合形成鲜明对比。
但是,因为HyperLogLog只会根据输入元素来计算基数,而不会存储输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集
{1,3,5,7,5,7,8}
那么这个数据集的基数集为{1,3,5,7,8}
,基数(不重复元素)为5。基数估计就是在误差可接受的范围内,快速计算基数。
-
常用命令
HyperLogLog指令都是pf(PF)开头,这是因为HyperLogLog的发明人是Philippe Flajolet,pf是他的名字的首字母缩写
-
pfadd key element [element ...]
:添加一个或多个元素到HyperLogLog类型的key中127.0.0.1:6379> flushdb OK 127.0.0.1:6379> pfadd subject java php redis mysql (integer) 1 127.0.0.1:6379> pfadd subject java php # 执行命令后,HyperLogLog估计的近似基数没有发生变化,返回0,否则返回1 (integer) 0 127.0.0.1:6379> pfadd subject java linux (integer) 1
-
pfcount key1 key2 ...
:获取多个HyperLogLog合并(去重)后元素的个数127.0.0.1:6379> flushdb OK 127.0.0.1:6379> pfadd subject redis mysql java (integer) 1 127.0.0.1:6379> pfadd subject1 redis java mongodb (integer) 1 127.0.0.1:6379> pfcount subject (integer) 3 127.0.0.1:6379> pfcount subject1 (integer) 3 127.0.0.1:6379> pfcount subject subject1 (integer) 4
-
pfmerge destkey sourcekey [sourcekey ...]
:将多个sourcekey
合并后放到destkey
127.0.0.1:6379> pfmerge destSubject subject subject1 OK 127.0.0.1:6379> pfcount destSubject (integer) 4
-
5.3 Geographic
-
简介
Redis3.2中增加了对GEO类型的支持。
GEO,Geographic,地理信息的缩写。
该类型就是元素的2维坐标,在地图上就是经纬度,redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
-
常用命令
-
geoadd key longitude latitude member [longitude latitude member ...]
:添加一个或多个地理位置(经度,纬度,名称)127.0.0.1:6379> flushdb OK 127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai # 上海的经纬度 (integer) 1 127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen 116.38 39.90 beijing # 添加重庆,深圳,北京的经纬度 (integer) 3 127.0.0.1:6379> type china:city # 查看geo类型,发现其实是zset zset 127.0.0.1:6379> zrange china:city 0 -1 1) "chongqing" 2) "shenzhen" 3) "shanghai" 4) "beijing" 127.0.0.1:6379> zrange china:city 0 -1 withscores 1) "chongqing" 2) "4026042091628984" 3) "shenzhen" 4) "4046432193584628" 5) "shanghai" 6) "4054803462927619" 7) "beijing" 8) "4069885332386336"
南北两极无法直接添加,一般会下载城市数据,直接通过java程序一次性导入
有效的经度从-180度到180度,有效的纬度从-85.05112878度到85.05112878度
当坐标位置超出指定范围时,该命令会返回一个错误
已经添加的数据,是无法再次往里面添加的
-
geopos key member [member ...]
:获取指定地区的坐标值127.0.0.1:6379> geopos china:city shanghai shenzhen guangzhou # 广州没有添加,所以为nil 1) 1) "121.47000163793563843" 2) "31.22999903975783553" 2) 1) "114.04999762773513794" 2) "22.5200000879503861" 3) (nil)
-
geodist key member1 member2 [m | km | ft | mi]
:获取两个位置之间的直线距离[m | km | ft | mi]
表示单位:m:米(默认值)
km:千米
mi:英里
ft:英尺
127.0.0.1:6379> geodist china:city beijing shenzhen "1945573.9752" 127.0.0.1:6379> geodist china:city beijing shenzhen km "1945.5740"
-
georadius key longitude latitude radius m|km|ft|mi
:以给定的经纬度为中心,找出某一半径内的元素[m | km | ft | mi]
表示单位:m:米(默认值)
km:千米
mi:英里
ft:英尺
127.0.0.1:6379> georadius china:city 110 30 1000 km # 以经纬度(110,30)为中心,半径为1000km范围内的geo元素 1) "chongqing" 2) "shenzhen"
-