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
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 ... 以此类推
- /> 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编码
- /> 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编码
- /> 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编码