Redis学习记录
1. Redis简介
Redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value数据库。
Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务,所有的命令在一个队列里排队等待被执行,不存在多个命令被同时执行的情况;
2. Redis数据结构
Redis 有 5 种基础数据结构,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。在Redis6中新增了3中数据类型,分别是Bitmaps、HyperLogLog和Geospatial。
每种数据结构都有自己的底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码;Redis这样设计有两个好处:第一:可以改进内部编码,而对外的数据结构和命令没有影响。第二 多种内部编码实现可以在不同的场景下发挥各自的优势。比如,ziplist比较节省内存,但是列表元素比较多的情况下,性能有所下降,这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist
2.1 String
Redis使用自己的简单动态字符串(simple dynamic string, SDS)的抽象类型。Redis中,默认以SDS作为自己的字符串表示。只有在一些字符串不可能出现变化的地方使用C字符串。
2.1.1 SDS概述
struct sdshdr {
// 用于记录buf数组中使用的字节的数目
// 和SDS存储的字符串的长度相等
int len;
// 用于记录buf数组中没有使用的字节的数目
int free;
// 字节数组,用于储存字符串
char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的。
};
SDS除了用来保存数据库中的字符串之外,SDS还被用作缓冲区(buffer),如AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区
2.1.2 SDS 的存储示例:
2.1.3 使用SDS而不使用c语言的string的好处:
- 简化Len获取
可以避免C中的顺序遍历,通过len属性直接获取字符串长度,复杂度为O(1); - 解决缓冲区溢出
避免了C中因分配内存不够而引起的溢出情况,Redis的SDS会自动对内存空间进行检测,进行内存的分配。
扩展buf空间策略:
修改之后总长度len<1MB: 总空间为2*len+1;
修改之后总长度len>=1MB: 总空间为len+1MB+1。
换句话说,预分配的空间上限是1MB,尽量为len。
- 减少内存重分配
字符串长度增加操作时,进行空间预分配
字符串长度减少操作时,惰性空间释放
当执行字符串长度缩短的操作的时候,SDS并不直接重新分配多出来的字节,而是修改len和free的值(len相应减小,free相应增大,buf的空间大小不变化),避免内存重分配。
SDS也提供直接释放未使用空间的API,在需要的时候,也能真正的释放掉多余的空间。
-
二进制安全
C字符串除了末尾之外不能出现空字符,否则会被程序认为是字符串的结尾。这就使得C字符串只能存储文本数据,而不能保存图像,音频等二进制数据。使用SDS就不需要依赖控制符,而是用len来指定存储数据的大小,所有的SDS API都会以处理二进制的方式来处理SDS的buf的数据。程序不会对buf的数据做任何限制、过滤或假设,数据写入的时候是什么,读取的时候依然不变。
-
兼容部分C字符串函数
SDS的buf的定义(字符串末尾为’\0’)和C字符串完全相同,因此很多的C字符串的操作都是适用于SDS->buf的。比如当buf里面存的是文本字符串的时候,大多数通过调用C语言的函数就可以。
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有库中的函数 | 可以使用一部分库的函数 |
2.1.4 常用命令
- 添加/修改数据
set key value
- 获取数据
get key value
- 删除数据
del key value
- 添加/修改多个数据
mset key1 value1 key2 value2 …
- 获取多个数据
mget key1 value1 key2 value2 …
- 获取数据字符个数(字符串长度)
strlen key
- 追加信息到原始信息后部(如果原始信息存在就追加,否则新建)
appand key value
- 数值自增
incr key //+1
incrby key increment //增加指定数值
incrbyfloat key increment //增加一个浮点数
- 数值自减
decr key //自减1
decrby key increment //减少指定数值
- 时效设置
setex key seconds value //增加、修改键值对并为其设定生命周期
2.1.5 String作为数值操作时的注意事项
- string在redis内部存储默认就是一个字符串,当遇到增减类操作incr,decr时会转成数值型进行计算
- redis所有的操作都是原子性的,采用单线程处理所有业务,命令是一个一个执行的,因此无需考虑并发带来的数据影响。
- 按数值进行操作的数据,如果原始数据不能转成数值,或超过了redis数值上线范围,将会报错。9223372036854775807 (java中long型数据最大值,Long.MAX_VALUE)
2.1.6 内部编码
字符串内部编码有3种:int 8个字节的长整型 embstr 小于等于39个字节的字符串 raw 大于39个字节的字符串。Redis会根据当前值的类型和长度决定使用哪种内部编码实现
2.1.7 使用场景
- 缓存功能
Redis作为缓存层,Mysql作为存储层,绝大部分的请求的数据都是从Redis中获取。由于Redis具有支持并发的特性,所以缓存通常能起到加速读写和降低后段压力的作用 - 开发提示
键名命名方式:业务名:对象名:id:[属性]作为键名;防作弊,按照不同维度计数,数据持久化到底层数据源 - 共享Session
- 限速
某一些网站限制一个ip地址不能在一秒钟之内访问超过n次也可以采用类似的思路;发送验证码等
2.2 list列表
2.2.1 list概述
Redis的列表类似于Java语言当中的LinkedList,但是还是存在着很大的区别的。
列表类型用来存储多个有序的字符串,一个列表最多存储2的32次方-1个元素,列表是一种比较灵活的数据结构,它可以灵活的充当栈和队列的角色,在实际开发上有很多应用场景
列表有两个特点:第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表。第二、列表中的元素可以是重复的
2.2.2 常用命令
- 在列表头部添加
lpush key value
- 在列表尾部添加
rpush key value
- 向某个元素前/后添加
linsert key before/after pivot value
- 在指定范围内查找
lrange key start end
索引下标有两个特点:第一,索引下标从左到右分别是0-n-1,从右到左是-1–n,第二,lrange的end选项包含了自身,这个和很多编程语言不包含end不太相同
- 获取列表指定索引下标的元素
lindex key index
- 获取列表长度
llen key
- 删除列表头元素
lpop key
- 删除列表尾元素
rpop key
- 删除指定元素
lrem key count value
- 按照索引范围修剪列表(删除范围外元素)
ltrim key start end
- 修改指定索引下标的元素
lset key index newValue
- 阻塞操作
brpop blpop key timeout
1 列表为空:如果timeout=3,那么客户端要等到3s后返回,如果timeout=0,客户端则阻塞等下去,如果添加了数据,客户端立刻返回
2 列表不为空:客户端立即返回
2.2.3 内部编码
Redis3.2版本的前,使用两种数据结构作为底层实现:
- 压缩列表zipList
当列表元素个数<list-max-ziplist-entries,同时list-max-ziplist-value(64字节),Redis会选用列表的内部实现来减少内存的使用 - 双向链表LinkedList
当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现
双向链表占用的内存比压缩链表要多,所以当创建新的列表键的时候,会优先考虑使用压缩列表,并且在有需要的时候,会转换成双向链表。
Redis3.2版本开始,Redis修改了list的底层实现,将压缩列表和双向链表结合,称之为quickList。
2.2.4 使用场景
- 消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列
- 文章列表
两个问题:第一,如果每次分页获取的文章个数较多,需要执行多次hgetall操作,此时考虑使用pipeline批量获取,或者考虑将文章数据序列化为字符串类型,使用mget批量获取。第二,分页获取文章列表时,lrange命令在列表两端性能较好,但是如果列表较大,获取列表中间范围元素的性能会变差,此时可以考虑二级拆分
2.2.5 开发提示
-
lpush+lpop=Stack(栈)
-
lpush+rpop=Queue(队列)
-
lpsh+ltrim=Capped Collection(有限集合)
-
lpush+brpop=Message Queue(消息队列)
2.3 set集合
2.3.1 概述
集合用来保存多个字符串元素,和列表不同的是不允许有重复元素,并且集合中元素是无序的
2.3.2 常用命令
- 添加元素
sadd key element
- 删除元素
srem key element
- 计算元素个数
scard key
- 判断元素是否存在
sismember key element
- 随机返回指定个数元素
srandmenmber key
- 随机弹出元素
spop key
- 获取所有元素
sembers key
- 求多个集合交集
sinter key
- 求多个集合并集
suinon key
- 求多个集合并集
saiff key
- 将交集,差集,并集结果保存
sinterstore destination key
sdiffstore destination key
suionstore destionation key
- 常用命令复杂度
2.3.3 内部编码
两种内部编码:
- intset(整数集合)
集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合内部的实现,从而减少内存的使用 - hashtable(哈希表)
当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现
2.3.4 使用场景
集合类型比较典型的应用场景是标签。
2.4 hash
2.4.1 概述
哈希类型是指键值本身又是一个键值对结构;
2.4.2 存储示例
2.4.3 常用命令
- 设置值
hset key field value
- 获取值
hget key field
- 删除field
hdel key field
- 计算field的个数
hlen key
- 批量设置field-value
hmset key field value
- 批量获取field-value
hmget key field
- 判断field是否存在
hexists key field
- 获取所有field
hkeys key
- 获取所有的value
hvals key
- 获取所有的field-value
hgetall key
提示:如果一定要获取全部的field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型
- 计算value的字符串长度
hstrlen key field
2.4.4 内部编码
hash内部编码有两种
- ziplist(压缩列表)
哈希元素个数<hash-max-ziplist-entries,所有值<hash-max-ziplist-value配置时,Redis会使用ziplist作为hash的内部实现,ziplist使用更加紧凑的结构实现多个元素存储,节省内存方面比hashtable更加优秀 - hashtable(哈希表)
当hash类型无法满足ziplist 条件时,选择,因为hashtable的读写时间度为O(1)
2.4.5 使用场景
- 缓存信息
原生字符串类型:每个属性一个键
优点:简单直观,每个属性都支持更新操作
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以一般不会在生产环境用 - 序列化字符串类型:将用户信息序列化后用一个键保存
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率
缺点:序列化和反序列化有一定的开销,同时每次更新属性,都需要把数据取出来反序列化,更新后再序列化到Redis中。 - 哈希类型:每个用户属性使用一对field-value,但是只用一个键保存
优点:简单直观,如果使用合理,可以减少内存空间的使用
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多的内存
2.5 zset有序集合
2.5.1 概述
有序集合就是在集合之上加了个score作为排序的依据
2.5.2 常用命令
- 添加成员
zadd key score memeber
有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间复杂度为O(log(n)),sadd的时间复杂度为O(1)
- 计算成员个数
scard key
- 计算某个成员的分数
zscore key member
- 计算成员的排名
zrank key member
- 删除成员
zrem key member
- 增加成员的分数
zincrby key increment member
- 返回指定排名范围的成员
zrange key start end
- 返回指定分数范围的成员
zrangebysore key min max
- 返回指定分数范围成员个数
zcount key min max
- 删除指定排名内的升序元素
zremrangebyrank key start end
- 删除指定分数范围的成员
zremrangebyscore key min max
- 交集
zinterstore destination numkeys key
- 并集
zunionstore destionation numkeys key
2.5.3 内部编码
序集合类型的内部编码有两种:
- ziplist(压缩列表)
当有序集合的元素个数小于zset-max-ziplist-entries配置,同时每个元素的值都小于zset-max-ziplist-value配置时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效的减少内存的使用 - skiplist(跳跃表)
当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因此此时ziplist的读写效率会下降
2.5.4 使用场景
有序集合比较典型的使用场景是排行榜系统
2.6 Redis常用操作
2.6.1 键管理
单个键管理
- 键重命名
rename key newkey
- 随机返回一个键
randomkey
- 键过期 -1 键没有设置过期时间 -2 键不存在
expire key seconds:键在seconds秒后过期
expire key itmestamp 键在秒级时间戳timestamp后过期
1 如果expire key的键不存在,返回结果为0
2 如果过期时间为负值,键会立即被删除,就如使用使用del命令一样
3 persist 命令可以将键的过期时间清除
4 对于字符串类型键,执行set命令会去掉过期时间,这个问题很容易在开发中被忽视
5 Redis不支持二级数据结构内部元素的过期功能,例如不能这列表类型的一个元素做过期时间设置
6 setex命令作为set+expire组合,不但原子执行,同时减少了网络通讯的时间
迁移键
迁移键功能非常重要,有move、dump+restore、migrate三组迁移键的方法,它们的实现方式以及使用场景不太相同
- 在Redis内部进行数据迁移
move
- 在不同的Redis实例之间进行数据迁移的功能
dump+restore
这个迁移分两步
1 在源Redis上,dump命令会将键值序列化,格式采用的是RDB格式
2 在目标Redis上,restore命令将上面的序列化的值进行复原,其中ttl参数代表过期时间
3 migrate 命令用于在Redis实例间进行数据迁移的
遍历键
Redis提供了两个命令遍历所有的键分别是keys和scan
- 全量遍历键
keys pattern
* 代表匹配任意字符
.代表匹配一个字符
[] 代表匹配一个字符
\x用来转义,例如要匹配星号,问号需要进行转义
大量容易造成阻塞
- 渐进式遍历
scan可以想象成只扫描一个字典中的一部分键,直到将字典中所有键遍历完毕
对应的命令还有hsan、sscan、zcan
渐进式遍历可以有效解决keys命令可能产生的阻塞问题,在有增删的时候,新来的键无法保证遍历到
2.6.2 数据库管理
- 切换数据库
select dbIndex
- flushdb/flushall
用于清除数据库 数据多的时候会出现阻塞
2.7 Bitmaps位操作
2.7.1 概述
Redis提供的Bitmaps这个“数据结构”可以实现对位的操作。Bitmaps本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作。只是单独提供了一套命令,可以把其看作是一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中称作偏移量。
合理地使用操作位能够有效地提高内存使用率和开发效率。
数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
2.7.2 常用命令
- 设置Bitmaps中某个偏移量的值
setbit key offset value
offset:偏移量从0开始
第一个参数表示你要操作的是第几个bit位,第二个参数表示你要将这个位设为何值,可选值只有0,1两个。
如果所操作的bit位超过了当前字串的长度,reids会自动增大字串长度。
- 获取Bitmaps中某个偏移量的值
getbit key offset
获取键的第offset位的值(从0开始算)
- 统计字符串从start字节到end字节比特值为1的数量
bitcount key start end
- 计算Bitmaps的交集的数量
bitop and destkey key[key…]
返回:保存到 destkey 的字符串(1字符等于8位)的长度,和输入 key 中最长的字符串长度相等。
- 计算Bitmaps的并集的数量
bitop or destkey key[key…]
返回:保存到 destkey 的字符串(1字符等于8位)的长度,和输入 key 中最长的字符串长度相等。
- 计算Bitmaps的非集的数量
bitop not destkey key
返回:保存到 destkey 的字符串(1字符等于8位)的长度,和输入 key 中最长的字符串长度相等。
- 计算Bitmaps的异或集的数量
bitop xor destkey key[key…]
返回:保存到 destkey 的字符串(1字符等于8位)的长度,和输入 key 中最长的字符串长度相等。
- 计算Bitmaps中第一个值为targetBit的偏移量
bitpos key targetBit [start][end]
返回:第一个值为的偏移量
2.7.3 使用场景
各种实时分析
存储与对象ID关联的布尔信息,要求高效且高性能
- 用户签到
我们以日期作为key,以用户ID作为位偏移(也就是索引),存储用户的签到信息(1为签到,0为未签到)。
2.7.4 优缺点
在大数据量前提下,如果有效数据占据大多数,Bitmaps能够节省很大的内存空间,但是如果有效数据少,Bitmaps 同样会将无效数据状态记录,会浪费空间。
但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。
set和Bitmaps存储一天活跃用户对比(独立用户比较少)
数据类型 每个userid占用空间 需要存储的用户量 全部内存量
集合类型 64位 100000 64位100000 = 800KB
Bitmaps 1位 100000000 1位100000000 = 12.5MB
2.8 HyperLogLog
2.8.1 概述
用来解决求集合中不重复元素个数的问题称为基数问题。比如UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题。
2.8.2 什么是基数
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
2.8.3 解决基数问题方案对比
- 数据存储在MySQL表中,使用distinct count计算不重复个数
- 使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。 - Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
2.8.4 常用命令
- 添加指定元素到 HyperLogLog 中
pfadd < key> [element …]
- 计算HLL的近似基数
pfcount [key …]
2.9 Geospatial
2.9.1 概述
Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
2.9.2 常用命令
- 添加地理位置(经度,纬度,名称)
geoadd< longitude> [longitude latitude member…]
- 获得指定地区的坐标值
geopos [member…]
- 获取两个位置之间的直线距离
geodist [m|km|ft|mi ]
- 以给定的经纬度为中心,找出某一半径内的元素,经度 纬度 距离 单位
- 单位:
m 表示单位为米[默认值]。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位