1.基本操作
flushdb -- 清空当前数据库
flushall -- 清空所有数据库
set key value
get key
keys q* --以q开头的数据
dbsize 存储多少key
exists xingxing,有就返回1,没有就是0
del xingxing **** .... 可以一次删除多个key,空格隔开
rename key
type xingxing 查看数据类型
2.数据类型
redis 数据类型 strings,hashes,lists,sets,sorted sets with range queries,bitmaps,
hyperloglogs,geospatial indexes with radius queries and streams
2.1String-存储类型
1、INT 整型 2、Float 单精度浮点型 3、String 字符串
2.1.1String-操作命令
1、INT 整型
2、Float 单精度浮点型
3、String 字符串
String-操作命令
getrange qingshan 0 1
strlen qingshan
append qingshan good ---追加
setnx qingshan pyy
mset qingshan 2673 huihui 666 ---
mget qingshan huihui
incr qingshan 一次增加1
incrby qingshan 100 一次增加100
decr qingshan 一次减1
decrby qingshan 100 一次减100
set mf 2.6
incrbyfloat mf 7.3
2.1.2String-存储原理
redisObject
String的三种编码
1、int,存储8个字节的长整型(long,2^63-1)
2、embstr,embstr格式的SDS,存储小于44个字节的字符串
3、raw,SDS,存储大于44个字节的字符串
SDS是什么?(Simple Dynamic String)
sdshdr5: 2^5=32byte(不用)
sdshdr8: 2^8=256byte
sdshdr16:2^16=65536byte=64KB
sdshdr32:2^32byte=4GB
为什么Redis要用SDS实现字符串?
1、内存空间预先分配 2、获取字符长度的时间复杂度O(n) 3、长度变更引起内存重新分配 4、用'\0'判断字符串结束
embstr和raw的区别?
embstr的只分配一次内存空间:RedisObject和SDS连续
raw 需要分配两次内存空间
int和embstr什么时候转化为raw?
1、int 数据不再是整数——raw
2、int大小超过了 long 的范围(2^63-1)——embstr
3、embstr长度超过了44个字节——raw
什么时候转化?会还原吗?
编码转换在 Redis 写入数据时完成,且转换过程不可逆, 只能从小内存编码向大内存编码转换(不包括重新set)
2.1.3String应用场景
1、缓存
Sting类型,缓存热点数据。例如 明星出滚,网站首页,报表数据等等,
可以显著提升热点数据访问的速度
2、分布式数据共享
String 类型, 因为Redis是分布式独立的服务,可以再多个应用之间共享
分布式Session
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
3、分布式锁 set NX EX
String类型的setnx方法,只有不存在时才能添加成功,返回true
public Boolean getLock(Object lockObject){
jedisUtil = getJedisConnetion();
boolean flag = jedisUtil.setNX(lockObj,1);
if(flag){
exprire(lockObj,10);
}
return flag;
}
public void releaseLock(Object lockObject){
del(lockObject);
}
4、分布式全局ID incr
INT类型,INCRBY,利用原子性
incrby userid 1000
(分库分表的场景,一次性拿一段)
5、计数器 incr
INT类型,INCR方法
例如:文章的阅读量,微博点赞书,允许一定的延迟,县写入Redis在定时同步到数据库
6、限流 incr
INT类型,INCR方法
以访问者的IP和其他信息作为key,访问一次增加一次计数,超过次数则返回false。
总结一下:利用redis本身的特性,和String内容的存储内容,以及提供的方法,我们可以用来达到很多的业务目的
3.HASH
一个KV怎么存储一张表的数据?
key分层
Hash 哈希
存储多个无序的键值对,最大存储数量2^32-1(40亿左右)。
String与Hash的区别
Hash特点: 1、节省内存空间 2、减少key冲突 3、取值减少性能消耗
Hash不适合的场景: 1、Field不能单独设置过期时间 2、需要考虑数据量分布的问题
3.1Hash基本操作
hset h1 f 6
hmset h1 a 1 b 2 c 3 d 4
hget h1 a
hmget h1 a b c d
hkeys h1
hvals h1
hgetall h1
hdel h1 a
hlen h1
del h1
3.2存储(实现)原理
Redis的Hash本身也是一个KV结构,是不是跟外层的哈希一样,用dictEntry实现呢
内层的哈希底层可以使用两种数据结构实现
ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)
hashtable:OBJ_ENCODING_HT(哈希表)
ziplist结构:
ziplist是一个经过特殊编码的,由连续内存块组成的双向链表。 它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存 储上一个节点长度和当前节点长度。
Hash-什么时候用ziplist?
1、 一个hash对象保存的field数量<512个
2、 一个hash对象中所有的field和value的字符串长度都<64byte
HashTable(dict)
hashtable-dict存储结构
从最底层到最高层dictEntry->dictht->dict。它是一个数组+链表的结构。
注意:dictht后面是NULL说明第二个ht还没用到。dictEntry*后面是NULL说明没有hash到这个地址。dictEntry后面是NULL,说明没有发生哈希冲突
问题:为什么要定义两个哈希表,其中一个不用呢?
redis的hash默认使用的是ht[0],ht[1]不会初始化和分配空间。
哈希表dictht是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决于它的大小(size属性)和它所保存的节点的数量(used属性)之间的比率:
1)比率在1:1时(一个哈希表ht只能存储一个节点entry),哈希表的性能最好
2)如果节点数量比哈希表的大小要大很多的话(这个比例用ratio表示,5表示平均一个ht存储5个entry),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不在存在。
如果单个哈希表的节点数量过多,哈希表的大小需要扩容。Redis里面的这种操作叫做rehash
rehash的步骤:
1. 为字符ht[1]哈希表分配空间。ht[1]的大小为第一个大于等于ht[0].used*2的2的N次方幂。比如已经使用了10000,那就是16384
2.将所有的ht[0]上的节点rehash到ht[1]上,重新计算hash值和索引,然后放入指定的位置。
3。当ht[0]全部迁移到了ht[1]之后,释放ht[0]的空间,将ht[1]设置为ht[0]表,并创建新的ht[1],为下次rehash做准备。
问题:什么时候出发扩容?
负载因子(源码dict.c):
static int dict_can_resize = 1;// 是否需要扩容
static unsigned int dict_force_resize_ratio = 5;// 扩容因子
扩容判断和扩容的操作大家可以自己看一下,跟HashMap一样,也有缩容。
总结一下,Redis的Hash类型,可以用ziplist和hashtable来实现。
3.3 应用场景
跟String一样,String可以做的事情,Hash都可以做。
存储对象类型的数据
比如对象或者一张表的数据,比String节省了更多key的空间,也更加便于集中管理。
补充案例:
4.List列表(有序)
4.1存储类型
存储有序的字符串(从左到右),元素可以重复。
最大存储数量2^32-1(40亿左右)。
4.2操作命令
4.3List存储原理(quicklist)
在早期版本中,数据量较小时用ziplist存储(特殊编码的双向链表),达到临界值时转换为linkedlist进行存储。
3.2版本之后,统一用quicklist来存储。quicklist存储了一个双向链表,每个节点都是一个ziplist,所以是zip与linkedlist的结合体
总体结构:
// quicklist.h 105行
typedef struct quicklist {
quicklistNode *head; // 指向双向链表的表头
quicklistNode *tail; // 表尾
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : QL_FILL_BITS; /* fill factor for individual nodes ziplist最大大小,对应list-max-ziplist-size*/
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off 压缩深度,对应list-compress-depth*/
unsigned int bookmark_count: QL_BM_BITS; // 4位,bookmarks数组的大小
quicklistBookmark bookmarks[]; // bookmarks 是一个可选字段
// quicklist 重新分配内存空间是使用,不使用时,不占空间
} quicklist;
redis.conf相关参数:
# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -5: max size: 64 Kb <-- not recommended for normal workloads
# -4: max size: 32 Kb <-- not recommended
# -3: max size: 16 Kb <-- probably not recommended
# -2: max size: 8 Kb <-- good
# -1: max size: 4 Kb <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2
# Lists may also be compressed.
# Compress depth is the number of quicklist ziplist nodes from *each* side of
# the list to *exclude* from compression. The head and tail of the list
# are always uncompressed for fast push/pop operations. Settings are:
# 0: disable all list compression
# 1: depth 1 means "don't start compressing until after 1 node into the list,
# going from either the head or tail"
# So: [head]->node->node->...->node->[tail]
# [head], [tail] will always be uncompressed; inner nodes will compress.
# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
# 2 here means: don't compress head or head->next or tail->prev or tail,
# but compress all nodes between them.
# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
# etc.
list-compress-depth 0
typedef struct quicklistNode {
struct quicklistNode *prev; // 前节点
struct quicklistNode *next; // 后节点
unsigned char *zl; // 指向实际的ziplist
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist 占16bit,最大65536个*/
unsigned int encoding : 2; //是否采用了LZF压缩算法压缩节点/* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
4.4应用场景
List 主要用在存储有序内容的场景
例如用户的消息列表,网站的公告列表,活动列表,博客的文章列表,评论列表等等。思路:存储所有字段,LANGE取出一页,按顺序显示。
队列/栈
List还可以当做分布式环境的队列/栈使用
List提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间。
BLPOP:BLPOP key timeout 移出并获取列表的第一个元素,如果列表没有元素,会阻塞别彪直到等待超时或发现可弹出元素为止
BLPOP:BLPOP key timeout 移出并获取列表的最后一个元素,如果列表没有元素,会阻塞别彪直到等待超时或发现可弹出元素为止
队列:先进先出:rpush blpop,左头右尾,左边出队列,后边进入队列
栈:先进后出,rpush,brpop
总结一下:
list存储有序的内容,用quicklist实现,本质上是数组+链表。
HashTabe也是数组加链表,只是内部编码结构不一样。
5.set集合
5.1存储类型
Set存储String类型的无序集合。 最大存储数量2^32-1(40亿左右)。
5.2操作命令
sadd myset a b c d e f g -- 添加一个或多个元素
smembers myset ---获取所有元素
scard myset --统计元素个数
srandmember myset --随机获取一个元素
spop myset --随机弹出一个元素
srem myset d e f --移除一个或多个元素
sismember myset a -- 查看元素是否存在
5.3存储原理
redis用intset或hashtable存储set。如果元素都是整数类型,就用inset存储。
typedef struct intset {
uint32_t encoding;// 编码类型 int16_t,int32_t,int64_t
uint32_t length;// 长度 最大长度:2^32
int8_t contents[];// 用来存储成员的动态数组
} intset;
如果不是整数类型,就用hashtable(数组+链表)
如果元素的个数超过512个,也会用hashtable存储。跟一个配置有关:
5.4应用场景
抽奖:
随机获取元素 spop myset
点赞、签到、打卡
这条微博的ID是t1001,用户ID是u3001.
用like:t1001来维护t1001这条微博的所有点赞用户
用户u3001点赞了这条微博:sadd like:t1001 u3001
用户u3001取消点赞:srem like:t1001 u3001
用户u3001是否点赞:sismember like:t1001 u3001
点赞的所有用户:smembers like:t1001
点赞数:scard like:t1001
商品标签
用 tags:i5001来维护商品所有的标签。
sadd tags:i5001 画面清晰细腻
sadd tags:i5001 真彩清晰显示屏
sadd tags:i5001 流畅至极
set-集合命令
set1 {a,b,c } set2 { b,c,d}
获取差集 sdiff set1 set2 {a}
获取交集(intersection ) sinter set1 set2 {b,c}
获取并集 sunion set1 set2 {a,b,c,d}
商品筛选
sadd brand:huawei p40
sadd os:android p40
sad screensize:6.0-6.24 p40
筛选商品:品牌华为,操作系统安卓,屏幕在6.0-6.24之间的
sinter brand:huawei brand:android screensize:6.0-6.24
6.zset 有序集合
6.1存储类型
sorted set 存储有序的元素,每个元素有个score,按照score从小到大排名。score相同时,按照key的ASCII码排序。
数据结构对比:
6.2操作命令
// 添加元素
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
// 获取全部元素
zrange myzset 0 -1 withscores // 小到大
zrevrange myzset 0 -1 withscores // 大到小
zrangebyscore myzset 20 30 根据分值区间获取元素
zrem myzset php cpp 移除元素 也可以根据score rank删除
zcard myzset 元素个数
zincrby myzset 5 python 分值递增
zcount myzset 20 60 根据分值统计个数(包括20和60)
zrank myzset python 获取元素rank
zscore myzset python 获取元素score
// 也有倒序的rev操作
6.3底层编码
默认使用ziplist编码(第三次加到,hash的小编码,quicklist的node,都是ziplist)
在ziplist的内部,按照score排序递增来存储。插入的时候要移动之后的数据。
如果元素数量大于等于128,或者任一member长度大于等于64字节使用skiplist+dict存储
什么是skiplist(跳表)?
我们先来看一下有序链表
在这样的一个链表中,如果我们要查找某个数据,那么需要从头开始逐个比较,直到找到包含数据的那个节点,或者找到第一个比给定数据打的节点为止。时间复杂度为O(n)。同样,当我们要插入新数据的时候,也要经历相同的查找过程,从而确定插入的位置,二分查找只适用于有序数组,不适用于链表
假如我们每相邻两个节点增加一个指针,让指针指向下下个节点(或者理解为有三个元素进入了第二层)
这样所有新增加的指针连成了一个新表,但包含的节点个数只有原来的一半
问题:是哪些元素运气这么好,进入到第二层?在插入一个数据的时候,决定要放到那一层,取决于一个算法,源码:t_zset.c 112行
/* Returns a random level for the new skiplist node we are going to create.
* The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
* (both inclusive), with a powerlaw-alike distribution where higher
* levels are less likely to be returned. */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
现在当我们想查找数据的时候,可以先沿着这个新链表进行查找。当碰到比待查数据打的节点时,再到下一层进行查找。
比如,我们想查找23,超找的路径是沿着标红的指针所指向的方向进行的:
1,23首先和7进行比较,再和19比较,比他们都大,继续向后比较。
2.但23和26比较的时候,比26要小,因此回到下面的链表(原链表),与19在第一层的下一个节点22比较
3.23比22要大,眼下面的指针继续向后和26比较,23比26小,说明待查询数据23不在原链表中
在这个比较过程中,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较,需要比较的节点数大概之后原来的一半,这就是跳跃表。
为什么不用AVL树或者红黑树?因为skiplist更加简洁。
因为level是随机的,得到的skiplist可能是这样的,有些在第四层,有些在第三层,有些在第二层,有些在第一层
我们看下Redis里面skiplist的实现
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
6.4应用场景
顺序会动态变化的列表
排行榜
例如百度热榜,微博热搜
id为6001的新闻点击数加1:zincrby hotNews:20251111 1 n6001
获取今天点击最多的15条:zrevrange hotNews:20251111 0 15 withscores
7 其他数据结构简介
7.1 BitMaps
BitMaps是在字符串类型上面定义的位操作。一个字节由8个二进制位组成。
set k1 a
获取value在offset处的值(a对应的ASCII码是97,转换为二进制数据是01100001)
getbit k1 0
修改二进制数据
setbit k1 6 1
setbit k1 7 0
get k1
统计二进制中1的个数
bitcount k1
获取第一个1或者0的位置
bitpos k1 1
bitpos k1 0
因为bit非常节省空间(1MB=8388608bit),可以用来做大数据量的统计
例如:在线人数统计,留存用户统计
setbit onlineusers 0 1
setbit onlineusers 1 1
setbit onlineusers 2 0
支持按位与,按位异或等等操作
BITOP AND destkey key [key ...] ,对一个或多个key 求逻辑与,并将结果保存到destkey中
BITOP OR destkey key [key ...].对一个或多个key 求逻辑或,并将结果保存到destkey中
BITOP XOR destkey key [key ...],对一个或多个key求逻辑异或,并将结果保存到destkey中
BITOP NOT destkey key 对给定key求逻辑非,并将结果保存到destkey中
计算出7天都在线的用户(假设用户编号有序,依次放在位图):
BITOP AND "7_days_both_online_users" "七天表"
应用场景:用户访问统计,在线用户统计
7.2Hyperloglogs
Hyperloglogs:提供了一种不太精确的基数统计方法,用来统计一个集合中不重复的元素个数,比如统计网站的UV,或者应用的日活,月活,存在一定的误差。
在Redis中实现的HyperLoglog,只需要12k内存就能统计2^64个数据。
7.3Geo
业务需求:给客户使用的客户端要求获取半径1公里以内的门店,那么我们就要把门店的经纬度保存起来。如果是直接把经纬度保存在数据库中,一个字段存经度,一个字段存纬度。计算距离比较复杂。Redis的GEO直接提供了这个方法
操作:增加地址位置信息,获取地址位置信息,计算两个位置的距离,获取指定费恩威诶内的地理位置集合等等。GeoTest.java
7.4Streams
5.0推出的数据类型,支持多播的可持久化的消息队列,用户实现发布订阅功能,借鉴了kafka的设计
8 总结
数据结构总结:
编码转换总结: