Redis
基本特性a) 非关系型的键值对数据库,可以根据键以O(1) 的时间复杂度取出或插入关联值
b) Redis
的数据是存在内存中的
c) 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的
d) 键值对中的值类型可以是string,hash,list,set,sorted set 等
e) Redis
内置了复制,磁盘持久化,LUA
脚本,事务,SSL, ACLs,客户端缓存,客户端代理等功能(6.0新特性)
f) 通过Redis
哨兵和Redis Cluster
模式提供高可用性
Redis
应用场景a) 计数器 可以对 String 进行自增自减运算,从而实现计数器功能。Redis
这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
b) 分布式ID生成 利用自增特性,一次请求一个大一点的步长如 incr 2000
,缓存在本地使用,用完再请求。
c) 海量数据统计 位图(bitmap):存储是否参过某次活动,是否已读谋篇文章,用户是否为会员, 日活统计。
d) 会话缓存 可以使用 Redis
来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
e) 分布式队列/阻塞队列 List 是一个双向链表,可以通过 lpush/rpush
和 rpop/lpop
写入和读取消息。可以通过使用brpop/blpop
来实现阻塞队列。
f) 分布式锁实现 在分布式场景下,无法使用基于进程的锁来对多个节点上的进程进行同步。可以使用 Redis
自带的 SETNX
命令实现分布式锁。
g) 热点数据存储 最新评论,最新文章列表,使用list 存储,ltrim
取出热点数据,删除老数据。
h) 社交类需求 Set 可以实现交集,从而实现共同好友等功能,Set通过求差集,可以进行好友推荐,文章推荐。
i) 排行榜 sorted_set可以实现有序性操作,从而实现排行榜等功能。
j) 延迟队列 使用sorted_set,使用 【当前时间戳 + 需要延迟的时长】做score, 消息内容作为元素,调用zadd
来生产消息,消费者使用zrangbyscore
获取当前时间之前的数据做轮询处理。消费完再删除任务 rem key member
Redis
的key 都是字符串(SDS
, simple dynamic string)类型,命令由客户端发送给服务端都会转换为字节流的形式,虽然看起来可能是数字、浮点数、或者字符串多种类型
> set 1 a // 数字key
> get 1
> set 0.5 b // 浮点key
> get 0.5
> set a c // 字符串key
> get a
以上key 发送到服务端最终都是以字节流的形式
SDS
的特点:
a)二进制安全的数据结构,安全体现在不会丢数据(c:char data[]="g\0ao"
,C 语言以"\0"
作为字符串结尾标记,业务数据存在"\0"
时存在安全问题)
b)内存预分配机制,防止频繁扩容产生的内存分配问题
c)兼容C语言的函数库
扩容示例:
redis:
sds:
free: 0
len: 3
char buf[]="gao" -> "gao123"
len: 3
addlen: 3
(len + addlen) * 2 = 12 //扩容,(现有长度+需要增加长度)*2
// 成倍扩容,当长度len=1024,再扩容每次增加1M,(所以使用 setbit 设置大点的空间,防止频繁扩容)
append,setbit
sds:
free: 6
len: 6
char buf[]="gao123"
设计思想:
K-V:map -> dict,
数据库:海量数据的存储
1、数组:O(1) 随机访问,下表
2、链表:O(n) 头结点,遍历
3、数:log(n) 优化的比较好的场景,二分查找
hash(key) 任意数据进行随机散列,并且把hash 值转换为一个自然数[大]
创建一个大的数组:arr[4]
hash(key) -> 自然数[大] % 4 = [0, arr.size-1], redis 就是这么干的
1、任意相同的输入,一定能得到相同的输出
2、不同的输入,可能得到相同的输出
把现实中无限的数据放到有限的集合中,肯定会产生hash 冲突
(k1,v1),(k2,v2),(k3,v3)
hash(k1) % 4 = 0
hash(k2) % 4 = 1
arr[0]->(k1,v1,next->null)
arr[1]->(k3,v3,next->k2)(k2,v2,next->null)
产生hash 冲突时,redis 使用的是头插法
redis 是比较基础的语言,没有那么多高级特性,没有那么多工具可以用,是redis 作者自己实现的
通过链表法来解决碰撞,java 中的hashMap 要复杂得多
key:string
value:string,hash,list,set,sorted set
Redis之String 类型数据结构直通车
Redis6.0
多线程,但是最终执行用户请求是在单线程中进行的,渐进式扩容很有必要
有趣命令:
> type [key] //查看key的类型
string
> object encoding 100 //key 所对应的值,在redis 底层是一个什么样的编码格式
"int"
> object encoding str
"embstr"
> object encoding 0.1
"int"
int / embstr 都是redis 对内存方面的优化
Redis
求模小优化:
任意数 % 2^n //对CPU 不友好,累除法
任意数 & (2^n - 1) //一次位运算
Redis
对于值是string 类型的底层编码结构分析:
//type [key],查看key 的类型
//object encoding [key],查值在底层的编码格式
//redis 中所有对象的封装
typedef struct redisObject {
unsigned type:4; //约束客户端命令,当前类型。位域的语法,占4个bit 位
unsigned encoding:4; //编码格式,同样占4个bit 位
unsigned lru:LRU_BITS; //24个bit 位,也就是3 个字节
int refcount; //引用计数法,4个字节
void *ptr; //数据真正存放的位置,8个字节
} robj;
redisObject 占16个字节的空间
cache line:64字节,64 - 16 = 48 字节,肯定可以利用起来
48个字节使用sdshdr8的数据结构存储,因为48在数据范围 2^5 ~ 2^8-1 之间
struct __attribute__ ((__packed__)) sdshdr8 { // 表示 2^5 ~ 2^8-1 的数据
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
sds本身消耗4个字节,其中len、alloc、flags各占一个字节,另外'\0'占一个字节,因为兼容C语言的函数库
最终:48 - 4 = 44 字节,用来存放实际的数据
对于string 类型的value 有结论如下:
value 值是string 类型的底层编码结构:
1、value值的长度小于等于20,尝试转换成数字转换成功,底层使用int 编码格式
2、value值不满足条件1,且长度小于等于44redis 底层使用embstr 编码结构
3、value值不满足1和2条件,使用row 编码结构也就是sds 的数据结构,简单动态字符串
扩展分析:
亿级日活的统计:
bit 的特点,除了0 就是1,所以可以用1个bit 位表示登陆状态
像这样1个字节就可以表示8个用户
1 1 0 1 0 1 0 1 1 1 0 1 0 0 1 0 1 -- 登陆状态
offset:0 1 2 3 4 5 6 7 8 9 ...
命令:setbit key offset 0|1
日期可以作为一个key 使用,“2022-5-26”
变种:userId 是数字时,可以作为偏移量使用
setbit a-bit-map userId 1
setbit a-bit-map userId 1
getbit a-bit-map 8
userId:
bit: 0/1 (默认值是0)
简单测试:
> setbit login_05_26 100 1
> setbit login_05_26 100 1
> setbit login_05_26 100 1
> getbit login_05_26 100 //获取日期5月26日用户100的活跃状态,id特别大可以减固定值优化
> setbit login_05_26 100 0 //恢复状态
> getbit login_05_26 100
> STRLEN login_05_26 //长度13 字节。100/8bit = 12.5 所以分配13个字节
> type login_05_26 //string 类型的数据
> get login_05_26 //可以执行命令,获取的数据是内存中的存储,没有实际意义
> setbit login_05_26 100 1
> setbit login_05_26 101 1
> setbit login_05_26 102 1
> setbit login_05_26 103 1
> BITCOUNT login_05_26 //统计bit位是1的个数:4
> strlen login_05_26 //13个字节
> BITCOUNT login_05_26 0 12 //一共13个字节,从索引0开始,所以是0和12。可以统计一部分数据根据索引
string 表示数据最大是512M,索引位是2^32-1。最多可表示这么多的用户
另一个扩展分析:
login_05_26: 0 1 0 1 0 1 1 1 0 1 0 1 1 0 0 1 1
login_05_27: 1 1 0 1 0 1 0 1 1 1 0 1 0 0 1 0 1
连续登陆情况:按位与再统计
> BITOP and login_05_26-27 login_05_26 login_05_27 //按位与操作,结果存到login_05_26-27中
> BITCOUNT login_05_26-27 //再做一次bitcount可以得到连续登陆的结果
login_05_24: 0 1 0 1 0 1 1 1 0 1 0 1 1 0 0 1 1
login_05_25: 1 1 0 1 0 1 0 1 1 1 0 1 0 0 1 0 1
login_05_26: 0 1 0 1 0 1 1 1 0 1 0 1 1 0 0 1 1
login_05_27: 1 1 0 1 0 1 0 1 1 1 0 1 0 0 1 0 1
login_05_28: 0 1 0 1 0 1 1 1 0 1 0 1 1 0 0 1 1
login_05_29: 1 1 0 1 0 1 0 1 1 1 0 1 0 0 1 0 1
login_05_30: 0 1 0 1 0 1 1 1 0 1 0 1 1 0 0 1 1
周活:按位或,有一天登陆就行
> BITOP or login_05_26-27-active login_05_26 login_05_27 //两天内有一天登陆就行
> BITCOUNT login_05_26-27-active
redis 源码有判断只能是0或1,按位与 按位或,效率非常高
汉明重量 按位操作 有兴趣的同学可以了解
/> help @list
LPUSH key element [element ...]
RPOP key
RPUSH key element [element ...]
LPOP key
BLPOP key [key ...] timeout
BRPOP key [key ...] timeout
BRPOPLPUSH source destination timeout
RPOPLPUSH source destination
LINDEX key index
LLEN key
LINSERT key BEFORE|AFTER pivot element
LRANGE key start stop
LREM key count element
LSET key index element
LTRIM key start stop
List是一个有序(按加入的时序排序)的数据结构,Redis采用quicklist(双端链表) 和 ziplist 作为List的底层实现。
可以通过设置每个ziplist的最大容量,quicklist的数据压缩范围,提升数据存取效率
list-max-ziplist-size -2 // 单个ziplist节点最大能存储 8kb ,超过则进行分裂,将数据存储在新的ziplist节点中
list-compress-depth 1 // 0 代表所有节点,都不进行压缩,1, 代表从头节点往后走一个,尾节点往前走一个不用压缩,其他的全部压缩,2,3,4 ... 以此类推
Redis - quicklist 数据结构:
Redis - ziplist 数据结构:
/> help @hash
HSET key field value [field value ...]
HGET key field
HMGET key field [field ...]
HKEYS key
HGETALL key
HVALS key
HEXISTS key field
HDEL key field [field ...]
HINCRBY key field increment
HINCRBYFLOAT key field increment
HLEN key
HSCAN key cursor [MATCH pattern] [COUNT count]
HSETNX key field value
HSTRLEN key field
Hash 数据结构底层实现为一个字典( dict ),也是RedisBb用来存储K-V的数据结构,当数据量比较小,或者单个元素比较小时,底层用ziplist存储,数据大小和元素数量阈值可以通过如下参数设置。
hash-max-ziplist-entries 512 // ziplist 元素个数超过 512 ,将改为hashtable编码
hash-max-ziplist-value 64 // 单个元素大小超过 64 byte时,将改为hashtable编码
Redis - hash 数据结构:
/> help @set
SADD key member [member ...]
SCARD key
SISMEMBER key member
SPOP key [count]
SDIFF key [key ...]
SINTER key [key ...]
SUNION key [key ...]
SMEMBERS key
SRANDMEMBER key [count]
SREM key member [member ...]
SMOVE source destination member
SUNIONSTORE destination key [key ...]
SDIFFSTORE destination key [key ...]
SINTERSTORE destination key [key ...]
SSCAN key cursor [MATCH pattern] [COUNT count]
Set 为无序的,自动去重的集合数据类型,Set 数据结构底层实现为一个value 为 null 的 字典( dict ),当数据可以用整形表示时,Set集合将被编码为intset数据结构。两个条件任意满足时 Set将用hashtable存储数据。1, 元素个数大于 set-max-intset-entries , 2 , 元素无法用整形表示
set-max-intset-entries 512 // intset 能存储的最大元素个数,超过则用hashtable编码
Redis - set 数据结构:
/> help @sorted_set
ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
ZCARD key
ZCOUNT key min max
ZINCRBY key increment member
ZRANGE key start stop [WITHSCORES]
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
ZRANK key member
ZREM key member [member ...]
ZREMRANGEBYRANK key start stop
ZREMRANGEBYSCORE key min max
ZREVRANGE key start stop [WITHSCORES]
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
ZREVRANK key member
ZSCAN key cursor [MATCH pattern] [COUNT count]
ZSCORE key member
ZSet 为有序的,自动去重的集合数据类型,ZSet 数据结构底层实现为 字典(dict) + 跳表(skiplist) ,当数据比较少时,用ziplist编码结构存储。
zset-max-ziplist-entries 128 // 元素个数超过128 ,将用skiplist编码
zset-max-ziplist-value 64 // 单个元素大小超过 64 byte, 将用 skiplist编码
Redis - zset 数据结构:
Redis - skiplist 数据结构: