一些常识
数据存在磁盘里,以磁盘的维度有两个指标
- 寻址:毫秒级
- 带宽:单位时间内可以有G/M字节流过去
数据存在内存里,内存有两个指标
- 寻址:纳秒级的
- 带宽:很大很大
所以从这里可以看来,在磁盘中获取数据和在内存中获取数据的速度相差非常大,有10万倍
数据库维护索引会让增删改变慢,创建索引后查询如果命中索引是比没有索引快的,但是当高并发情况下,一万个查询同时就来,就会受带宽影响导致查询变慢;所以是从两个方面出发考虑的:
- 一个是数据库自身查询
- 另一个是硬件I/O影响
每秒操作次数(OPS)
- redis:10w/s级别
- 关系型数据库:千/s级别
介绍
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)
Redis和Memcache区别
- memcache:value没有类型区分,存储复杂对象采用json,从复杂对象获取数据需要先获取全量数据再使用代码解析
- redis:有类型区分(不重要),server中每种类型都有自己的取值方法,使取值更方便
memcache -> redis:计算向数据移动
为什么快
单进程,使用epoll(数据的“顺序”为文件描述符到达的先后顺序进行挨个处理)
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 使用多路I/O复用模型,非阻塞IO;
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
内核的演变
- BIO(阻塞):socket产生的fd(文件描述符),在读fd时数据包还未到(not ready)时,read命令就不能返回(阻塞住),所以会抛出更多线程获取新的socket(JVM线程成本1MB【栈】),如果是单核CPU,那么某一时间点时间片只能一个线程处理,线程更多切换线程也存在成本问题(CPU忙着调度去了),同时更多的线程会占用更多的内存空间
- NIO(同步非阻塞):单线程循环遍历(轮循发生在用户空间)处理文件描述符,遍历到fd有数据则进行处理,完成后再继续遍历(遍历和处理都是线程自己)
- select(多路复用):将fd交给内核select,线程调用一次select一次性返回目前有数据的fd给线程,线程再去read该fd
- epoll(select增强版):优化select数据拷来拷去的问题,当描述符在epoll函数中为ready时,才可以去read,减少线程不必要的调用;epoll会准备一块内核态和用户态共享空间(mmap),共享空间增删改操作由系统内核实现,减少数据传输(fd全放在红黑树中,ready的fd再转移到链表,线程直接从链表中获取fd进行read),用户空间wait(),当链表中有数据时从阻塞变为非阻塞获取到文件描述符再进行read操作,并且用户空间线程对于新来的链接可以调用epoll将文件描述符添加或移除共享空间中的红黑树
- 图一:
- 原始文件传输:文件经过内核缓冲区,经过程序拷贝再发送给接收方(网卡)
- sendfile(零拷贝):文件在内核缓冲区经过sendfile函数的处理直接到接收方,省略了程序的拷贝过程
- 图二:
- kafka:网卡数据过来结果nmap共享空间将数据写入文件,再通过sendfile函数获取文件内容给消费者
默认16个数据库,默认选择0号库
一些命令
- keys * #查询所有key - flushalll / flushdb #清库(慎用) - help [命令] #查看具体命令操作文档(可自动补齐) help @[类型] #查看类型的操作 #k=key v=value - set k1 ooxx nx #如果k1不存在新增k1并赋值ooxx set k2 hello xx #如果k2存在则更新k2=hello mset k1 v1 k2 v2 #批量添加 mget k1 k2 #批量获取
字符串
# GETRANGE
#向k1追加字符串,使用GETRANGE获取索引范围数据(正向【从0开始】/负向【从-1开始】索引)
127.0.0.1:6379> set k1 hello nx
OK
127.0.0.1:6379> APPEND k1 " world"
(integer) 11
127.0.0.1:6379> GETRANGE k1 6 10
"world"
127.0.0.1:6379> GETRANGE k1 6 -1
"world"
127.0.0.1:6379> GETRANGE k1 0 -1
"hello world"
# SETRANGE(从某key索引位置覆盖新的值)
127.0.0.1:6379> get k1
"hello world"
127.0.0.1:6379> SETRANGE k1 6 ARMIN
(integer) 11
127.0.0.1:6379> get k1
"hello ARMIN"
# STRLEN(获取某key的字符长度)
127.0.0.1:6379> STRLEN k1
(integer) 11
# type(获取key的类型,key前面的命令是哪个group的type即为group值)
127.0.0.1:6379> help set
SET key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
summary: Set the string value of a key
since: 1.0.0
group: string
127.0.0.1:6379> set k1 99
OK
127.0.0.1:6379> type k1
string
数值
INCR
自增(原子性)# OBJECT(获取key的value类型,int类型可以做数值操作)
127.0.0.1:6379> OBJECT encoding k2
"embstr"
127.0.0.1:6379> OBJECT encoding k1
"int"
# INCR(对某key做+1操作)
# INCRBY(对某key加指定数值)
127.0.0.1:6379> INCR k1
(integer) 100
127.0.0.1:6379> get k1
"100"
127.0.0.1:6379> INCRBY k1 22
(integer) 122
127.0.0.1:6379> get k1
"122"
# DECR(某key-1)
# DECRBY(某key减指定数值)
127.0.0.1:6379> DECR k1
(integer) 121
127.0.0.1:6379> DECRBY k1 22
(integer) 99
# INCRBYFLOAT(对某key加指定数值的浮点数)
127.0.0.1:6379> INCRBYFLOAT k1 0.5
"99.5"
# redis是二进制安全的(类型的变化都是为了存储的安全,redis采用二进制进行存储)
127.0.0.1:6379> set k3 jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
OK
127.0.0.1:6379> OBJECT encoding k3
"embstr"
127.0.0.1:6379> APPEND k3 jjjjj
(integer) 44
127.0.0.1:6379> OBJECT encoding k3
"raw"
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> set k1 hello
OK
127.0.0.1:6379> STRLEN k1
(integer) 5
127.0.0.1:6379> set k2 9
OK
127.0.0.1:6379> OBJECT encoding k2
"int"
127.0.0.1:6379> STRLEN k2
(integer) 1
127.0.0.1:6379> APPEND k2 999
(integer) 4
127.0.0.1:6379> get k2
"9999"
127.0.0.1:6379> OBJECT encoding k2
"raw"
127.0.0.1:6379> INCR k2
(integer) 10000
127.0.0.1:6379> OBJECT encoding k2
"int"
127.0.0.1:6379> STRLEN k2
(integer) 5
127.0.0.1:6379> set k3 a
OK
127.0.0.1:6379> get k3
"a"
127.0.0.1:6379> STRLEN k3
(integer) 1
127.0.0.1:6379> APPEND k3 中 #由于链接redis客户端机器编码采用的utf-8则一个中文占3个字节
(integer) 4
127.0.0.1:6379> get k3
"a\xe4\xb8\xad"
127.0.0.1:6379> STRLEN k3
(integer) 4
# redis-cli --raw 使用当前编码进行客户端启动(默认不带参数为ascii码,超过ascii码表示范围直接按十六进制显示)
armin@xiaobawangxuexiji-2 ~ % redis-cli --raw
127.0.0.1:6379> get k3
a中
# GETSET(某key设置新的值并返回老的值,相较get&set通信上面减少了一次I/O)
127.0.0.1:6379> GETSET k1 ARMIN
hello
127.0.0.1:6379> get k1
ARMIN
# MSETNX(以k v形式批量添加,如果k存在则该条语句的所有操作均失败【保障原子性】)
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> MSETNX k1 a k2 b
1
127.0.0.1:6379> mget k1 k2
a
b
127.0.0.1:6379> MSETNX k2 c k3 d
0
127.0.0.1:6379> mget k1 k2 k3
a
b
bitmap
setbit
bitcount
bitpos
bitop
# SETBIT 给某key二进制下标赋值二进制0/1(offset为某key二进制下标第几位,value值为二进制0/1)
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> help SETBIT
SETBIT key offset value
summary: Sets or clears the bit at offset in the string value stored at key
since: 2.2.0
group: string
127.0.0.1:6379> setbit k1 1 1 #给k1二进制位第1位(从0开始)赋值1【0100 0000】
0
127.0.0.1:6379> STRLEN k1
1 #占用一个字节
127.0.0.1:6379> get k1 #[0100 0000]ascii码解码后的值
@
127.0.0.1:6379> setbit k1 7 1 #[0100 0000]->[0100 0001]第七位赋值1
0
127.0.0.1:6379> STRLEN k1
1
127.0.0.1:6379> get k1 #[0100 0001]->ascii=A
A
127.0.0.1:6379> setbit k1 9 1 #[0100 0001]+[0100 0000]第九位赋值1
0
127.0.0.1:6379> STRLEN k1 #[0100 0001][0100 0000]占两个字节
2
127.0.0.1:6379> get k1 #ascii码翻译 A@
A@
# BITPOS (获取某key的二进制位在索引字符开始位与索引字符开始结束位的第几个二进制位)
127.0.0.1:6379> help bitpos
BITPOS key bit [start] [end]
summary: Find first bit set or clear in a string
since: 2.8.7
group: string
127.0.0.1:6379> BITPOS k1 1 0 0 #k1[01000001 01000000] 的第一个二进制1在第0个字节与第0个字节见的第1个二进制位
1
127.0.0.1:6379> BITPOS k1 1 1 1 #k1[01000001 01000000] 的第一个二进制1在第1个字节与第1个字节见的第9个二进制位
9
127.0.0.1:6379> BITPOS k1 1 0 1 #k1[01000001 01000000] 的第一个二进制1在第0个字节与第1个字节见的第1个二进制位
1
# BITCOUNT (某key从字符索引位到字符索引位出现二进制1的次数)
127.0.0.1:6379> help BITCOUNT
BITCOUNT key [start end]
summary: Count set bits in a string
since: 2.6.0
group: string
127.0.0.1:6379> BITCOUNT k1 0 1 #k1[0100000101000000]从字符0到字符1共出现3次二进制1
3
127.0.0.1:6379> BITCOUNT k1 0 0
2
127.0.0.1:6379> BITCOUNT k1 1 1
1
# BITOP (某些key进行按位操作)
127.0.0.1:6379> help BITOP
BITOP operation destkey key [key ...]
summary: Perform bitwise operations between strings
since: 2.6.0
group: string
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> help BITOP
BITOP operation destkey key [key ...]
summary: Perform bitwise operations between strings
since: 2.6.0
group: string
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> SETBIT k1 1 1
0
127.0.0.1:6379> SETBIT k1 7 1
0
127.0.0.1:6379> get k1 #[0100 0001]
A
127.0.0.1:6379> SETBIT k2 1 1
0
127.0.0.1:6379> SETBIT k2 6 1 #[0100 0010]
0
127.0.0.1:6379> get k2
B
127.0.0.1:6379> BITOP and andkey k1 k2 #k1 k2按位与结果放在andkey
1
127.0.0.1:6379> get andkey #[0100 0000]
@
127.0.0.1:6379> BITOP or orkey k1 k2 #k1 k2按位或运算
1
127.0.0.1:6379> get orkey #[0100 0011]
C
需求分析:
有用户系统,统计用户登录天数,且窗口随机
分析: 如果采用关系型数据库至少两个字段:用户id、登录时间(假设这两个字段公8个字节,那么一年就有365x8字节,再加上用户数量,这张表足够庞大) 解决方案: 使用redis bitmap;按每年400天算,仅占50个字节 使用方法: setbit [用户key] 1 1 #表示该用户第二天已登录 setbit [用户key] 7 1 #表示用户第八天已登录 setbit [用户key] 364 1 #表示用户第365天已登录 bitcount [用户key] -2 -1 #查询用户最近两周登录次数,负数表示从某位到最前的字节索引 不但节省空间,且二进制位运算在计算机中是最快的
我的商城618做活动,登录就送礼物,大库存备多少送礼物,假设商城2亿用户
分析: 用户主要有:僵尸用户/冷热用户/忠诚用户;进行活跃用户统计且可以随机窗口查询 比如:1号-3号 连续登录 去重 解决方案: 使用redis bitmap 每天作为key每个用户约定好各自的二进制位,当天登录则在当天的key中该用户所占二进制位中赋值1 使用方法: setbit 20220901 1 1 setbit 20220902 1 1 setbit 20220902 7 1 bitop or destkey 20220901 20220902 bitcount destkey 0 -1 #计算destkek字节第0位到字节最后一位出现1的次数,每个1都代表一个用户,这样就能统计处1号-2号登录的用户数
栈:同向命令
队列:反向命令
lpush
:数据从左往右依次插入rpush
:数据从右往左依次插入lpop
:从最左边弹出一个元素rpop
:从最右边弹出一个元素lrange
:获取集合中开始结束索引位中所有数据lindex
:取出key值集合中某索引的元素lset
:给key值集合某索引设置新的值lindex
:获取key值集合索引位下数据lset
:设置key值某索引位数据为新的值lrem
:移除key值多少个(绝对值:+索引前两个;-索引后两个)某元素值linsert
:在key值某元素前或后插入元素某(如元素出现多次仅在第一次出现的位置插入)llen
:统计key值元素个数ltrim
:删除key值两索引两端外数据127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> lpush k1 a b c d e f
(integer) 6
127.0.0.1:6379> rpush k2 a b c d e f
(integer) 6
127.0.0.1:6379> lpop k1
"f"
127.0.0.1:6379> lpop k1
"e"
127.0.0.1:6379> lpop k1
"d"
127.0.0.1:6379> rpop k2
"f"
127.0.0.1:6379> lpop k2
"a"
# LRANGE key startIndex endIndex
127.0.0.1:6379> LRANGE k2 0 -1 #也有正向与负向索引-1往后
1) "b"
2) "c"
3) "d"
4) "e"
127.0.0.1:6379>
127.0.0.1:6379> lpush k1 a b c d e f
(integer) 6
127.0.0.1:6379> LRANGE k1 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "b"
6) "a"
127.0.0.1:6379> rpush k2 a b c d e f #rpush从右往左,最后一位添加的数据为a
(integer) 6
127.0.0.1:6379> LRANGE k2 0 -1 #栈结构:先进后出
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
# LINDEX LSET
127.0.0.1:6379> LINDEX k1 0 #获取k1 0号索引位数据
"f"
127.0.0.1:6379> LSET k1 3 xxxx #设置k1 3号索引位数据为xxxx
OK
127.0.0.1:6379> LRANGE k1 0 -1
1) "f"
2) "e"
3) "d"
4) "xxxx"
5) "b"
6) "a"
# LREM
127.0.0.1:6379> LRANGE k3 0 -1
1) "d"
2) "6"
3) "a"
4) "5"
5) "c"
6) "4"
7) "a"
8) "3"
9) "b"
10) "2"
11) "a"
12) "1"
127.0.0.1:6379> LREM k3 2 a #移除k3中2个a(2为正数则从正向低索引位开始移除)
(integer) 2
127.0.0.1:6379> LRANGE k3 0 -1
1) "d"
2) "6"
3) "5"
4) "c"
5) "4"
6) "3"
7) "b"
8) "2"
9) "a"
10) "1"
# LINSERT
127.0.0.1:6379> LINSERT k3 after 6 a #在k3集合中6元素后插入一个a
(integer) 11
127.0.0.1:6379> LINSERT k3 before 3 a
(integer) 12
127.0.0.1:6379> LRANGE k3 0 -1
1) "d"
2) "6"
3) "a"
4) "5"
5) "c"
6) "4"
7) "a"
8) "3"
9) "b"
10) "2"
11) "a"
12) "1"
127.0.0.1:6379> LREM k3 -2 a #移除k3中2个a(-2为负数则从负向低索引位开始移除)
(integer) 2
127.0.0.1:6379> LRANGE k3 0 -1
1) "d"
2) "6"
3) "a"
4) "5"
5) "c"
6) "4"
7) "3"
8) "b"
9) "2"
10) "1"
# LTRIM
127.0.0.1:6379> LTRIM k3 2 -2 #删除索引位两端外数据(索引2的左边,-2的右边)
OK
127.0.0.1:6379> LRANGE k3 0 -1
1) "a"
2) "5"
3) "c"
4) "4"
5) "3"
6) "b"
7) "2"
阻塞 单播队列(先进先出)
blpop
:出栈key值一个数据且设置阻塞时间,0为一直阻塞直到获取到值
多个客户端链接服务端使用blpop获取同一个key值数据,当该key未设置值时,突然有一个客户端设置了该key一个值,这时第一个链接客户端发送blpop命令的会获取到数据结束阻塞,其他客户端继续阻塞,直到有新的值赋值且轮到该客户端获取则结束阻塞,前提设置阻塞时间都为0(一直阻塞)
在没有hash
需要对复杂对象分别存储信息时,可以采用如下方式
# 以string结构,key细分某对象的属性,最后使用该对象名称*进行获取所有属性,从而进行分别获取数据
127.0.0.1:6379> set sean::name 'armin'
OK
127.0.0.1:6379> get sean::name
"armin"
127.0.0.1:6379> set sean::age 18
OK
127.0.0.1:6379> keys sean*
1) "sean::age"
2) "sean::name"
127.0.0.1:6379> keys *age #*可以左右匹配所有
1) "sean::age"
hset
:设置key的hash存储类似java中的map,只是key为map的key
hmset
:同时设置多个值
hget
:获取某key中某属性的值
hmget
:同时获取多个key中哪些属性的值
hkeys
:获取某key中所有属性
hvals
:获取某key中所有属性对应的所有值
HINCRBYFLOAT
:某key中某属性的value增加指定浮点数*(如从int转为float后仅可使用fload的指令)*
127.0.0.1:6379> hset sean name armin #设置hash key sean 中的 name=armin
(integer) 1
127.0.0.1:6379> hmset sean age 16 address chengdu #sean: age=16 & address=cehngdu
OK
# hget hmget
127.0.0.1:6379> hget sean name
"armin"
127.0.0.1:6379> hmget sean name age
1) "armin"
2) "16"
# hkeys hvals
127.0.0.1:6379> hkeys sean #获取sean key对应的所有属性
1) "name"
2) "age"
3) "adress"
127.0.0.1:6379> hvals sean #获取sean key中所有属性对应的值
1) "armin"
2) "16"
3) "chengdu"
# hgetall
127.0.0.1:6379> hgetall sean #获取sean key中所有的数据
1) "name"
2) "armin"
3) "age"
4) "16"
5) "adress"
6) "chengdu"
# HINCRBYFLOAT
127.0.0.1:6379> HINCRBYFLOAT sean age 0.5 #sean key 中 age +0.5
"16.5"
127.0.0.1:6379> HINCRBYFLOAT sean age -1 #sean key 中 age -1
"15.5"
应用场景:
- 商品的详情页中的属性与值
- 商品被浏览的次数,加入购物车、点赞、收藏次数等(可计算且批处理同时获取这些属性与值)
数据去重,不维护插入排序,乱序的
SMEMBERS
:获取某key中所有元素*(尽量不用,会消耗redis所在主机的网卡吞吐量,那么其他进程或redis实例链接请求会变慢)*
sadd
:某key添加一个或多个元素
srem
:移除某key中哪些元素
SINTER
:对某些key进行交集得出结果
SINTERSTORE
:对某些key进行交集结构存入定义的某key*(所有的操作一步完成,相较于SINTER完成该操作直接在服务端一步完成省略了更多的IO操作)*
SUNION
:对某些key得出并集
SUNIONSTORE
:对某些key的并集存入新定义的key
SDIFF
:差集,以哪个key为准该key放在最左边
# sadd
127.0.0.1:6379> sadd k1 tom sean peter ooxx tom xxoo #六个元素添加了五个(去重)
(integer) 5
127.0.0.1:6379> SMEMBERS k1 #获取k1所有元素
1) "tom"
2) "ooxx"
3) "sean"
4) "xxoo"
5) "peter"
# srem
127.0.0.1:6379> srem k1 ooxx xxoo #移除元素
(integer) 2
127.0.0.1:6379> SMEMBERS k1
1) "peter"
2) "tom"
3) "sean"
127.0.0.1:6379> sadd k2 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd k3 4 5 6 7 8
(integer) 5
# SINTER
127.0.0.1:6379> SINTER k2 k3
1) "4"
2) "5"
127.0.0.1:6379> SINTERSTORE dest k2 k3 #k2 k3交集存入dest key
(integer) 2
127.0.0.1:6379> SMEMBERS dest
1) "4"
2) "5"
# SUNION SUNIONSTORE
127.0.0.1:6379> SUNION k2 k3 #并集(已去重)
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
# SDIFF
127.0.0.1:6379> SDIFF k2 k3 #差集
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> SDIFF k3 k2
1) "6"
2) "7"
3) "8"
随机事件
SRANDMEMBER
SRANDMEMBER key count
key的取值:
- 正数:取出一个去重的结果集(不能超过已有集)
- 负数:取出一个带重复的结果集,一定满足你要的数量
- 0:不返回
应用场景:
- 抽奖:10个奖品,用户大于或小于10,中奖是否重复
- 假设
k1
中有20个用户,count=10,则随机中奖人数为10且不重复- 假设
k1
中有20个用户,count=-10,则中奖次数为10,允许重复中奖- 假设
k1
中有3个用户,count=-10,则中奖次数为10,允许重复中奖- 解决家庭斗争问题
- 当孩子出生要改孩子起名字,三大姑八大爷都想起名字,将孩子名字放入集合count设置为较集合更大的值进行随机获取名字,哪个名字出现的次数多就用哪个名字
SPOP
:随机弹出一个元素,该元素弹出后就不再该set
集合中了
应用场景:
- 公司年会抽奖,所有员工存入一个set集合,每当抽取一个奖品随机弹出一个用户
SMISMEMBER
:检查给定的值是不是特定key的成员
SMISMEMBER key member [member...] #member值可以为多个,返回多个结果
127.0.0.1:6379> sadd k1 tom ooxx xxoo xoxo oxox xoox oxxo
(integer) 7
127.0.0.1:6379> SMISMEMBER k1 tom
1) (integer) 1
127.0.0.1:6379> SMISMEMBER k1 za tom
1) (integer) 0 #false
2) (integer) 1 #true
有序去重集合(默认升序),其内部结构包含三部分且物理内存按左小右大且不随命令而改变
命令
zadd
:向某key添加元素并赋值其分值
ZRANGE
:根据索引取出某key的集索引内集合
ZRANGEBYSCORE
:根据某key的分值范围取出元素*(left<=score<=rigth || left
ZREVRANGE
:ZRANGE
的倒序
# zadd ZRANGE ZRANGEBYSCORE
127.0.0.1:6379> zadd k1 8 apple 2 banana 3 orange
(integer) 3
127.0.0.1:6379> ZRANGE k1 0 -1
1) "banana"
2) "orange"
3) "apple"
127.0.0.1:6379> ZRANGE k1 0 -1 withscores
1) "banana"
2) "2"
3) "orange"
4) "3"
5) "apple"
6) "8"
127.0.0.1:6379> ZRANGEBYSCORE k1 3 8 #left<=score<=rigth
1) "orange"
2) "apple"
127.0.0.1:6379> ZRANGEBYSCORE k1 (3 8 #left
1) "apple"
需求分析
根据分数的由低到高取出前两名的数据
127.0.0.1:6379> ZRANGE k1 0 1 1) "banana" 2) "orange"
根据分数的由高到低取出前两名的数据
127.0.0.1:6379> ZREVRANGE k1 0 1 1) "apple" 2) "orange" #错误示范 127.0.0.1:6379> ZRANGE k1 -2 -1 #此处返回的数据还是由小到大 1) "orange" 2) "apple"
ZSCORE
:获取某key某元素的分值
ZRANK
:获取某key某元素的排名
127.0.0.1:6379> ZSCORE k1 apple #分值
"8"
127.0.0.1:6379> ZRANK k1 apple #排名(从零开始升序)
(integer) 2
ZINCRBY
:对某key某元素分值进行增加指定数值,可以是浮点数,增加后排序随之自动更新
127.0.0.1:6379> ZRANGE k1 0 -1 withscores
1) "banana"
2) "2"
3) "orange"
4) "3"
5) "apple"
6) "8"
127.0.0.1:6379> ZINCRBY k1 2.5 banana
"4.5"
127.0.0.1:6379> ZRANGE k1 0 -1 withscores
1) "orange"
2) "3"
3) "banana"
4) "4.5"
5) "apple"
6) "8"
需求分析
- 歌曲榜单的top10排名,用户每点击一次该歌曲的元素分值+1,top10显示为
ZREVRANGE
0~9
ZUNIONSTORE
:指定数量key与具体key并集且输出到某key
交集差级类似,且命令与set结构类似s->z
127.0.0.1:6379> zadd k1 80 tom 60 sean 70 baby
(integer) 3
127.0.0.1:6379> zadd k2 60 tom 100 sean 40 armin
(integer) 3
127.0.0.1:6379> ZUNIONSTORE unkey 2 k1 k2 #默认权重都为1,且相同元素分值进行求和(sum)
(integer) 4
127.0.0.1:6379> ZRANGE unkey 0 -1 withscores
1) "armin"
2) "40"
3) "baby"
4) "70"
5) "tom"
6) "140"
7) "sean"
8) "160"
127.0.0.1:6379> ZUNIONSTORE unkey1 2 k1 k2 weights 1 0.5 #k1权重1,k2权重0.5(原分值的1/2)
(integer) 4
127.0.0.1:6379> ZRANGE unkey1 0 -1 withscores
1) "armin"
2) "20"
3) "baby"
4) "70"
5) "sean"
6) "110"
7) "tom"
8) "110"
127.0.0.1:6379> ZUNIONSTORE unkey1 2 k1 k2 aggregate max #相同元素分值取最大值进行并集
(integer) 4
127.0.0.1:6379> ZRANGE unkey1 0 -1 withscores
1) "armin"
2) "40"
3) "baby"
4) "70"
5) "tom"
6) "80"
7) "sean"
8) "100"
底层原理
排序是怎么实现的,增删改查的速度?
增删改性能类似链表,查询效率优于链表,综合性能最优
跳跃表*(skip list)*
- 增删改均以修改链表指针进行排序
- 新增一个元素,首先会根据最高层比较自己的分数,然后再到倒数第二次进行比较直到最后一层确定位置,进行对前后数据的指针进行修改,且随机生成该元素的层数,生成层数元素也是修改当前层进行比较后进行指针的修改,前面元素指向当前元素,当前元素指向上一个元素指向之前的元素(典型的空间换时间:消耗存储空间进而提升查询效率)
通过nc
命令链接服务端进行指令传输,让通信成本更低
armin@xiaobawangxuexiji-2 ~ % nc localhost 6379
keys * #查询所有key
*0 #返回没有key
set k1 hello #设置新k1
+OK #返回结果
#使用客户端获取数据
127.0.0.1:6379> get k1
"hello"
也可以通过echo
命令进行数据传输
# -e 可识别\n换行符;|为管道符;设置k2=99且自增1,再获取k2
armin@xiaobawangxuexiji-2 ~ % echo -e "set k2 99\nincr k2\n get k2" | nc localhost 6379
+OK #设置k2成功
:100 #k2自增成功返回
$3 #get k2返回宽度为3
100 #获取k2
子进程
echo $$ | more
echo $BASHPID | more
#$$ 优先级高于 |
只有在发布(PUBLISH
)端发送之前另一端订阅(SUBSCRIBE
)了才会在前者发送消息后收到消息
#发布端
127.0.0.1:6379> PUBLISH ooxx hello
(integer) 0
127.0.0.1:6379> PUBLISH ooxx helloxiaobawang
(integer) 1
#接收端
127.0.0.1:6379> SUBSCRIBE ooxx
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ooxx"
3) (integer) 1 #由于第一条发送时并未订阅ooxx所以未收到第一条消息
1) "message"
2) "ooxx"
3) "helloxiaobawang"
使用场景
- 直播聊天室、聊天软件聊天,可查看近三天聊天记录或更早的聊天记录
实现方式
- 实时聊天可使用redis发布订阅功能
- 查看最近三天可使用redis用
sorted_set
缓存三天的数据,时间为分值,记录为具体的值,使用ZREMRANGEBYRANK/ZREMRANGEBYSCORE
命令可以移除不需要的分值范围数据,比如超过三天时间的分值- 查看更早的数据使用人数会少很多,所以可以对数据进行数据入库,后续查询出日期在三天前的数据。由于入库数据消息可能会很多可以采用消息队列的形式进行入库,并实时缓存到三天内数据的
sorted_set
集合根据上面的原理可以对系统架构进行解耦实现,如下图
- 发布客户端发布消息到redis
- 订阅客户端订阅redis中实时消息
- 另一个redis服务订阅发布到目标redis服务中的数据,并实时缓存近三天消息
- 另一个处理数据入库的微服务订阅发布目标redis的服务,获取消息发送到消息队列对消息进行入库处理
客户端需要开启事务时需要在一连串要绑定事务的命令前首先执行mutli(开启事务)
,最后在执行exec(执行事务)
那么期间的命令都在一个事务中;如果有多个客户端同时开启事务,客户端1删除某key,客户端2获取某key,客户端2能否获取到key取决于客户端2的exec
指令是否比客户端1先到达,哪个客户端的exec
指令先到的就先执行该客户端这同一事物中的所有指令
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> set k1 aaa #事务中的数据会先放到队列中,当执行时根据队列一并执行
QUEUED
127.0.0.1:6379(TX)> set k2 bbb
QUEUED
127.0.0.1:6379(TX)> exec #执行
1) OK
2) OK
#当含删除的事务先执行
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> del k1
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 1
#后执行的获取事务,那么该key不存在
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> exec
1) (nil)
查看事务指令:help @transactions
watch
:在事务开启之前使用该命令实现监控该条事务中的k1,当图中k1在执行到get
命令时k1发生了改变则该食物的所有操作会被取消
#先开启的事务
127.0.0.1:6379> set k1 aaa
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> keys *
QUEUED
127.0.0.1:6379(TX)> EXEC #后执行,由于watch k1,k1在下面事务中已经发生了改变所以该事务中操作都被取消了
(nil)
#后开启的事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 bbb
QUEUED
127.0.0.1:6379(TX)> EXEC #先执行
1) OK
#最后获取k1是事务先执行的赋值
127.0.0.1:6379> get k1
"bbb"
为什么redis不支持回滚
解决缓存击穿问题
缓存击穿
问题也叫热点Key问题
,就是一个被高并发访问并且缓存重建业务较复杂(意味着对数据库压力相对较大)的key突然失效了(可以理解为redis的缓存突然无了),无数的请求访问会在瞬间给数据库带来巨大的冲击解决方案:
- 对于热点查询,设置key-null,为其缓存空对象
- 使用布隆过滤器
小的空间解决大量数据匹配的过程
维护一个bitmap
。布隆算法可以客户端实现,也可以直接使用redis中额外引用的,bitmap
可以由客户端维护,也可以由redis维护。大致是,bitmap
中标记已有的数据,没有的数据直接由维护的bitmap
过滤掉,从而不再访问数据库,存在一定概率误标记,但是成本低
缓存数据不重要(不是全量数据)缓存应该随着访问变化(热数据)
redis里的数据怎么能随着业务变化,只保留热数据,因为内存大小式有限的,也就是瓶颈:使用淘汰策略
实际情况中常用LRU:如果设置过期key较多使用volatile-lru,反之使用allkeys-lru
Redis keys过期有两种方式:被动和主动方式。
当一些客户端尝试访问它时,key会被发现并主动的过期。
当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。
具体就是Redis每秒10次做的事情:
- 测试随机的20个keys进行相关过期检测。
- 删除所有已经过期的keys。
- 如果有多于25%的keys过期,重复步奏1.
这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。
在复制AOF文件时如何处理过期
为了获得正确的行为而不牺牲一致性,当一个key过期,
DEL
将会随着AOF文字一起合成到所有附加的slaves。在master实例中,这种方法是集中的,并且不存在一致性错误的机会。然而,当slaves连接到master时,不会独立过期keys(会等到master执行DEL命令),他们任然会在数据集里面存在,所以当slave当选为master时淘汰keys会独立执行,然后成为master。
写在前面
在
linux
系统中:
- 父子进程数据是隔离的
- 父进程可以让子进程看到父进程的数据,采用
export
修饰export
的环境变量,父子进程的修改互补影响,影响范围仅为自身进程
redis如何在非阻塞的情况下对某一时刻数据进行持久化
- redis父进程使用
fork()
系统调用,创建一个子进程,其为redis中创建那一刻中虚拟地址的拷贝,其虚拟地址同样指向相同的内存地址,则有两个虚拟地址引用了该物理地址fork()
实现了copy on write
内存机制,在父进程修改数据时,首先会在内存中写入修改的值,并将指针从原来的地址指向新的地址,所以子进程的数据不会改变,还是那一刻的数据,从而达到非阻塞保存时点快照
fork()特点:速度快,占用空间小
主动(同步阻塞):比如关机维护时使用
被动(异步非阻塞):fork()
系统调用,可在配置文件中配置触发规则
#在多少秒后至少有多少个key发生变化触发写入磁盘操作(可配置多条)
save <seconds> <changes> #默认开启,可增加 save "" 表示禁用
dbfilename dump.rdb
对dump.rdb
改为需要的文件名The working directory
注释,重写其dir
路径即可只要触发了RDB操作,等待该操作完成后,下一次的RDB触发才可执行
弊端:
dump.rdb
文件,需要人为干预进行备份优点:类似java
中的序列化,恢复的速度相对较快
适当的配置redis最大存储空间maxmemory可以提升RDB的效率,最大空间过大会导致RBD时间过长
RDB与AOF可以同时开启,如果开启了AOF只会使用AOF进行数据恢复
优点:数据丢失少
弊端:操作日志文件的体量会越来越大,从而导致恢复慢
针对弊端redis根据让日志只记录增量且合并重复命令的原理对AOF进行了优化:
4.0前:重写日志文件,删除抵消的命令合并重复的命令,得到一个缩小版的纯指令的日志文件
4.0后:重写日志文件,将某一刻老的数据RDB到AOF文件中,将增量的指令以追加的方式追加到AOF文件中
AOF是一个混合体,既利用了RDB的快速恢复也利用了日志的全量特点
appendonly no -> yes
开启AOFappendfilename "appendonly.aof"
可自定义aof
文件中刷写的策略
appendfsync always
:每来一条指令调用一次内核将数据直接刷写到磁盘,最多丢失一条数据(一条指令正在写宕机了);数据最可靠appendfsync everysec
:每秒触发一次I/O对buffer
进行flush
到aof
文件中,可能会导致接近一个buffer
缓存内容的丢失;数据较为可靠appendfsync no
:每当内核buffer
缓冲区内容填满时触发一次I/O,对缓冲区的数据flush
写入磁盘aof
文件;最多丢失一块buffer
缓冲区内容数据,换来I/O成本最少no-appendfsync-on-rewrite
:yes/no(默认)
当redis抛出一个子进程进行AOF重写或者RDB重写,允不允许序号3的策略进行aof
文件的写入,yes可能会导致一部分性能阻塞,no可能导致一些数据的丢失aof-use-rdb-preamble yes
:开启重写日志文件,见上4.0后;如果aof
文件以REDIS
开头的则是重写过的混合体文件(默认开启)BGREWRITEAOF
(重写)规则
auto-aof-rewrite-percentage [百分比(省略%)]
:记录6.2.
命令达到的百分比触发重写auto-aof-rewrite-min-size [文件大小(mb)]
:首次aof
文件达到该大小且满足6.1.
自动触发重写,并且redis会记录重写后此时文件大小lastSize
,当文件超过lastSize
的设置百分比6.1.
时再次出发重写,依次类推自动重写操作(此变量仅初始化启动redis有效,如果是redis恢复时,则lastSize
等于初始aof
文件大小)