Redis知识点总结

1. redis简介

随着Web2.0的时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据。加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战。redis是一个用C语言编写的、开源的、基于内存运行并支持持久化的、高性能的NoSQL数据库,也是当前热门的NoSQL数据库之一。redis中的数据大部分时间都是存储在内存中的,适合存储频繁访问、数据量比较小的数据。

1.NoSQL适用场景:

高并发:

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

Redis知识点总结_第1张图片
高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

Redis知识点总结_第2张图片

应用场景

缓存

缓存现在几乎是所有大中型网站都在用的必杀技,合理利用缓存提升网站的访问速度,还能大大降低数据库的访问压力。Redis 提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在 Redis 用在缓存的场合非常多。

排行榜

Redis 提供的有序集合Zset数据类结构能够实现复杂的排行榜应用。

计数器

视频网站的播放量,每次浏览 +1,并发量高时如果每次都请求数据库操作无疑有很大挑战和压力。Redis 提供的string incr 命令来实现计数器功能,内存操作,性能非常好,非常适用于这些技术场景。

分布式会话

相对复杂的系统中,一般都会搭建 Redis 等内存数据库为中心的 session 服务。因为现在是微服务部署,一个服务使用一个mysql或一个服务使用一个主机,若session保存在本地,下次请求分发到另一台主机,则不共享

分布式锁

在并发高的场合中,可以利用 Redis 的 setnx 功能来编写分布式的锁,如果设置返回 1,说明获取锁成功,否则获取锁失败。

社交网络

点赞、踩、关注/被关注,共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库不适合这种类型的数据,Redis 提供的哈希,集合等数据结构能很方便的实现这些功能。

最新列表

Redis 列表结构,LPUSH 可以在列表头部插入一个内容 ID 作为关键字,LTRIM 可以用来限制列表的数量,这样列表永远为 N ,无需查询最新的列表,直接根据 ID 去到对应的内容也即可。

异步队列

消息队列是网站经常用的中间件,如 ActiveMQ,RabbitMQ,Kafaka 等流行的消息队列中间件,主要用于业务解耦,流量削峰及异步处理试试性低的业务。Redis 提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。利用LIST可以实现队列的功能。

任务队列的特性:可靠性 消息有序 不可重复消费

有序性:LPUSH RPOP,一般使用list结构作为队列,LPUSH 生产消息,RPOP消费消息。当RPOP没有消息的时候,要适当sleep一会再重试。

实时消费:生产者发送消息到队列,队列不会通知消费者,所以需要轮询读取消耗CPU,或者需要sleep。list有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

重复消费问题:生产者生产每个mess的时候对应一个全局唯一ID,redis消费完成后记录,下次消费的时候去求存在与否

消息可靠性:RPOPLPUSH 原子操作,从list1中读取存入list2中,处理完后才能后删除list2中该数据,宕机后从list2中重新读取。

生产一次消费多次:使用pub/sub主题订阅者模式,可以实现1:N的消息队列。但是这样在消费者下线的情况下,生产的消息会丢失(因为消费者消费一半下线,东西会丢失),得使用专业的消息队列如rabbitmq等。

延时队列: 使用Zset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

但是如果生产速度持续快于消费速度,就会造成性能瓶颈。

2.NoSQL不适用场景

  • 需要事务支持

  • 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。

  • 用不着sql的和用了sql也不行的情况,请考虑用NoSql

3.常见NoSQl数据库

  • Memcache
    很早出现的NoSql数据库,数据都在内存中,一般不持久化,支持简单的key-value模式,支持类型单一,一般是作为缓存数据库辅助持久化的数据库。

  • redis
    几乎覆盖了Memcached的绝大部分功能,数据都在内存中,支持持久化,主要用作备份恢复,除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等,一般是作为缓存数据库辅助持久化的数据库。

  • MongoDB
    高性能、开源、模式自由(schema free) 的文档型数据库,数据都在内存中, 如果内存不足,把不常用的数据保存到硬盘,虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能。支持二进制数据及大型对象,可以根据数据的特点替代RDBMS,成为独立的数据库。或者配合RDBMS,存储特定的数据。

4.数据库应用的发展历程:

  • 单机数据库时代: 一台电脑安装一个数据库实例 sqlserver等,一个应用对应一个数据库服务器

  • Memcached缓存、水平切分时代: 应用和数据库服务器中加一个缓存解决经常访问的数据的访问效率问题,但是数据量较大。水平切分解决数据量大的问题:将一个数据库服务器拆分为多个,一个应用按照不同业务存储在不同服务器

  • 读写分离时代: 但是若是订单表,本身就特别大,并发能力特别弱,所以可以拆分,几个表负责读,几个负责写,几个负责删除,谁闲着找谁,用同步机制保证ACID,负责写的为主负责读的为从,主同步到从。

  • 分表分库时代(集群) : 某一天或某一周的数据存在一个表中,同类型的数据不存储在同一个表中,因为数据量太大了

前面的都是关系型数据库时代以表为单位存储。

  • 非关系型数据库(NoSql) : 彻底改变底层存储机制。不再采用关系数据模型,而是采用聚合数据结构存储数据。 redis、mongoDB、HBase等。

5. redis特点

  1. 支持数据持久化
    redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

  2. 支持多种数据结构
    redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。

  3. 支持数据备份
    redis支持数据的备份,即master-slave模式的数据备份。

2. redis基本操作

1.启动redis

安装目录下的 redis.windows.conf 配置文件可修改

  1. 服务端启动: redis-server.exe redis.windows.conf启动服务端 /进入对应的安装目录,执行:./bin/redis-server
  2. 客户端启动: redis-cli –h IP地址 –p 端口 //默认IP本机 端口6379

2.关闭redis服务

  1. 非正常关闭,ps -ef | grep -i redis,kill -9 PID对查询到的id进行强制关闭
  2. 通过shutdown命令在客户端进行正常关闭

3.其他基本命令

1.测试redis服务的性能:redis-benchmark

2.查看redis服务是否正常运行:客户端连接服务器之后发送 ping 如果正常 服务器返回pong

3.查看redis服务器的统计信息:
info 查看redis服务的所有统计信息
info [信息段] 查看redis服务器的指定的统计信息,如:info CPU

4.redis的数据库实例(16个库) :

作用类似于mysql的数据库实例,但是redis中的数据库实例只能由redis服务来创建和维护,开发人员不能修改和自行创建数据库实例。各个库不能自定义命名,只能用序号表示,redis中各个库是完全独立的,使用时最好一个应用使用一个redis实例,不建议一个redis实例中保存多个应用的数据。

默认情况下,redis会自动创建16个数据库实例,并且给这些数据库实例进行编号,从0开始,一直到15,使用时通过编号来使用数据库;

可以通过配置文件redis.conf,指定redis自动创建的数据库个数,redis的每一个数据库实例本身占用的存储空间是很少的,所以也不会造成存储空间的太多浪费。

默认情况下,redis客户端连接的是编号是0的数据库实例;可以使用select index切换数据库实例。

127.0.0.1:6379select 1
OK
127.0.0.1:6379[1]set k1 v1
OK
127.0.0.1:6379[1]get k1
"v1"
127.0.0.1:6379[1]select 0
OK
127.0.0.1:6379get k1
(nil) 

5.查看当前数据库实例中所有key的数量:dbsize
例:

127.0.0.1:6379[1]dbsize
(integer)  1

6.清空数据库key:flushdb

7.清空所有的数据库key:flushall

8.查看redis中所有的配置信息:config get *
查看redis中的指定的配置信息:config get port
例:

127.0.0.1:6379[1]config get port
1)  "port"
2)  "6379" 

redis英文版命令大全:https://redis.io/commands

redis中文版命令大全:http://redisdoc.com/

3. redis的五种数据结构及bitmap

程序是用来处理数据的,redis数据库是用来存储数据的;程序处理完的数据要存储到redis中,不同特点的数据要存储在redis中不同类型的数据结构中。

字符串:字符串类型是redis中最基本的数据结构,它能存储任何类型的数据,包括二进制数据,序列化后的数据,JSON化的对象甚至是一张图片。最大512M。单key单value,做简单的键值对缓存。

list列表:redis列表是简单的字符串列表,按照插入顺序排序,元素可以重复。可以添加一个元素到列表的头部(左边)或者尾部(右边),底层是个链表结构。单key多value,value有序,顺序和放入的顺序有关,存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据。

set集合:redis的set是string类型的无序无重复集合。单key多value,value无序,交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集。

hash:hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。单key,value是对象{k1:v1,k2:v2…} 。

zset:redis 有序集合zset和集合set一样也是string类型元素的集合,且不允许重复的成员。不同的是zset的每个元素都会关联一个分数(分数可以重复),redis通过分数来为集合中的成员进行从小到大的排序。单key多value value有序按某个标准排序,可以获取排名前几名的用户。

1. redis中有关key的操作命令

a) 查看数据库中的key:keys pattern

*:匹配0个或者多个字符

keys * 查看数据库中所有的key

keys k*:查看数据库中所有以k开头的key

keys h*o:查看数据库中所有以h开头、以o结尾的key

?: 匹配1个字符

keys h?o: 查看数据库中所有以h开头、以o结尾的、并且中间只有一个字符的key

[]:匹配[]里边的1个字符

keys h[abc]llo:查看数据库中所有以h开头以llo结尾,并且h后边只能取abc中的一个字符的key

b) 判断key在数据库中是否存在:exists key

exists k1如果存在,则返回1;如果不存在,则返回0

exists key [key key ....] 返回值是存在的key的数量

exists k1 k2 k3 hello

c) 移动指定key到指定的数据库实例:move key index 用的不多,因为一个项目对应一个数据库实例,最好数据不要交流

move k 1

d) 查看指定key的剩余生存时间:

tt- key time to live

tt- k1

###如果key没有设置生存时间,返回-1,如果key不存在,返回-2

e) 设置key的最大生存时间:expire key seconds
expire k2 20

f) 查看指定key的数据类型:type key
type k1 五种数据类型

g) 重命名key: rename key newkey
rename hello k2

h) 删除指定的key:de- key [key key .....]
返回值是实际删除的key的数量,key不存在就忽略
de- k1 k2 k3 k4

2. redis中有关string类型数据的操作命令: 单key-单value

a) 将string类型的数据设置到redis中:set 键 值
set zsname zhangsan
set zsage 30 如果key已经存在,则后来的value会把以前的value覆盖掉

b) 从redis中获取string类型的数据:get 键
get zsname

c) 追加字符串:append key value
返回追加之后的字符串长度,如果key不存在,相当于set,新创建一个key,并且把value值设置为value。
set phone 1389999
append phone 8888 所以phone变为了13899998888

d) 获取字符串数据的长度:strlen key
strlen phone

e) 将字符串数值进行加1运算:incr key
返回加1运算之后的数据,如果key不存在,首先设置一个key,值初始化为0,然后进行incr运算。要求key所表示value必须是数值,否则,报错
incr zsage

f) 将字符串数值进行减1运算:decr key
返回减1运算之后的数据,如果key不存在,首先设置一个key,值初始化为0,然后进行decr运算。要求key所表示value必须是数值,否则,报错

decr zsage

g) 将字符串数值进行加offset运算:incrby key offset
返回加offset运算之后的数据,如果key不存在,首先设置一个key,值初始化为0,然后进行incrby运算。要求key所表示value必须是数值,否则,报错
incrby zsage 10

h) 将字符串数值进行减offset运算:decrby key offset
返回减offset运算之后的数据,如果key不存在,首先设置一个key,值初始化为0,然后进行decrby运算。要求key所表示value必须是数值,否则,报错
decrby zsage 10

i) 闭区间获取字符串key中从startIndex到endIndex的字符组成的子字符串:

getrange key startIndex endIndex

下标自左至右,从0开始,依次往后,最后一个字符的下标是字符串长多-1;
字符串中每一个下标也可以是负数,负下标表示自右至左,从-1开始,依次往前,最右边一个字符的下标是-1
zsname = zhangsan
getrange zsname 2 5 angs
getrange zsname 2 -3 angs
getrange zsname 0 -1 zhangsan

j) 用value覆盖从下标为startIndex开始的字符串,能覆盖几个字符就覆盖几个字符:

setrange key startIndex value
setrange zsname 5 xiaosan // zhangxiaosan
setrange zsname 5 lao // zhanglaoosan

k) 设置字符串数据的同时,设置它最大生命周期 当key存在时覆盖 :setex key seconds value
setex k1 20 v1

l) 设置string类型的数据value到redis数据库中,当key不存在时设置成功,否则,则放弃设置:setnx key value
setnx zsage 20

m) 批量将string类型的数据设置到redis中:mset 键1 值1 键2 值2 .....
mset k1 v1 k2 v2 k3 v3 k4 v4 k5 v5

n) 批量从redis中获取string类型的数据:mget 键1 键2 键3.....
mget k1 k2 k3 k4 k5 k6 zsname zs age totalRows 没有的话返回nil

o) 批量设置string类型的数据value到redis数据库中,当所有key都不存在时设置成功,否则(只要有一个key已经存在) ,则全部放弃设置: msetnx 键1 值1 键2 值2 .....
msetnx kk1 vv1 kk2 vv2 kk3 vv3 k1 v1

3. redis中有关list类型数据的操作命令:单key-多有序value

一个key对应多个value,多个value之间有顺序,最左侧是表头,最右侧是表尾;
每一个元素都有下标,表头元素的下标是0,依次往后排序,最后一个元素下标是列表长度-1;
每一个元素的下标又可以用负数表示,负下标表示从表尾计算,最后一个元素下标用-1表示;
元素在列表中的顺序或者下标由放入的顺序来决定。通过key和下标来操作数据。

a) 将一个或者多个值依次插入到列表的表头(左侧) :lpush key value [value value .....]
lpush list01 1 2 3 结果:3 2 1
lpush list01 4 5 结果:5 4 3 2 1

b) 获取指定列表中指定下标区间的元素 闭区间:lrange key startIndex endIndex
lrange list01 1 3 结果:4 3 2
lrange list01 1 -2 结果: 4 3 2
lrange list01 0 -1 结果:5 4 3 2 1

c) 将一个或者多个值依次插入到列表的表尾(右侧) :rpush key value [value value .....]
rpush list02 a b c 结果:a b c
rpush list02 d e 结果:a b c d e
lpush list02 m n 结果: n m a b c d e

d) 从指定列表中移除并且返回表头元素:lpop key
lpop list02

e) 从指定列表中移除并且返回表尾元素:rpop key
rpop list02

f) 获取指定列表中指定下标的元素:lindex key index
lindex list01 2 结果:3

g) 获取指定列表的长度:llen key
llen list01

h) 根据count值移除指定列表中跟value相等的数据:

lrem key count value

count0:从列表的左侧移除count个跟value相等的数据;

count<0:从列表的右侧移除count个跟vlaue相等的数据;

count=0:从列表中移除所有跟value相等的数据

lpush list03 a a b c a d e a b b 结果:b b a e d a c b a a
lrem list03 2 a 结果:b b e d c b a a
lrem list03 -1 a 结果:b b e d c b a
lrem list03 0 a 结果:b b e d c b

i) 截取指定列表中指定下标区间的元素组成新的列表,并且赋值给key:

ltrim key startIndex endIndex
lpush list04 1 2 3 4 5 结果:5 4 3 2 1
ltrim list04 1 3
lrange list04 0 -1 结果:4 3 2

j) 将指定列表中指定下标的元素设置为指定值: lset key index value
lset list04 1 10

k) 将value插入到指定列表中位于pivot元素之前/之后的位置:

linsert key before/after pivot vlaue
linsert list04 before 10 50
linsert list04 after 10 60

4. redis中有关set类型数据的操作命令:单key-多无序value

一个key对应多个vlaue,value之间没有顺序,并且不能重复,通过key直接操作集合,元素无下标。

a) 将一个或者多个元素添加到指定的集合中:sadd key value [value value ....]

如果元素已经存在,则会忽略。
返回成功加入的元素的个数
sadd set01 a b c a 结果:a b c
sadd set01 b d e

b) 获取指定集合中所有的元素:smembers key
smembers set01

c) 判断指定元素在指定集合中是否存在:sismember key member
sismember set01 f 存在,返回1,不存在,返回0

d) 获取指定集合的长度:scard key
scard set01

e) 移除指定集合中一个或者多个元素:srem key member [member .....]
srem set01 b d m 不存在的元素会被忽略,返回成功移除的个数

f) 随机获取指定集合中的一个或者多个元素:srandmember key [count] 抽奖活动
count0:随机获取的多个元素之间不能重复
count<0: 随机获取的多个元素之间可能重复
sadd set02 1 2 3 4 5 6 7 8
srandmember set02 随机输出一个元素
srandmember set02 3 随机输出3个元素 且不能重复
srandmember set02 -3随机输出3个元素 且可以重复

g) 从指定集合中随机移除一个或者多个元素:spop key [count]
spop set02 如果不写count 则随机移除一个元素 返回移除的值

h) 将指定集合中的指定元素移动到另一个元素:smove source dest member
smove set01 set02 a 将集合set01的a元素移动到set02中

i) 获取第一个集合中有,但是其它集合中都没有的元素组成的新集合:差集

sdiff key key [key key ....]

sdiff set01 set02 set03 返回set01中有 set02和set03中都没有的元素

j) 获取所有指定集合中都有的元素组成的新集合:

sinter key key [key key ....]
sinter set01 set02 set03 返回set01 set02和set03中都有的元素

k) 获取所有指定集合中所有元素组成的大集合:

sunion key key [key key .....]
sunion set01 set02 set03 返回set01 set02和set03中所有的元素集合

5. redis中有关hash类型数据的操作命令:单key:field-value field-value field-value

a) 将一个或者多个field-vlaue对设置到哈希表中:hset key filed1 value1 [field2 value2 ....]
如果key field已经存在,把value会把以前的值覆盖掉

hset stu1001 id 1001
hset stu1001 name zhangsan age 20

b) 获取指定哈希表中指定field的值:hget key field
hget stu1001 id

c) 批量将多个field-value对设置到哈希表中:和hset用法完全相同 hmset key filed1 value1 [field2 value2 ....]
hmset stu1002 id 1002 name lisi age 20

d) 批量获取指定哈希表中的field的值:hmget key field1 [field2 field3 ....]
hmget stu1001 id name age

e) 获取指定哈希表中所有的field和value:hgetal- key
hgetal- stu1002

f) 从指定哈希表中删除一个或者多个field:hde- key field1 [field2 field3 ....]
hde- stu1002 name age

g) 获取指定哈希表中所有的filed个数:hlen key
hlen stu1001

h) 判断指定哈希表中是否存在某一个field:hexists key field
hexists stu1001 name

i) 获取指定哈希表中所有的field列表:hkeys key
hkeys stu1001

j) 获取指定哈希表中所有的value列表:hvals key
hvals stu1001

k) 对指定哈希表中指定field值进行整数加法运算:hincrby key field int
hincrby stu1001 age 5

l) 对指定哈希表中指定field值进行浮点数加法运算:hincrbyfloat key field float
hset stu1001 score 80.5
hincrbyfloat stu1001 score 5.5

m) 将一个field-vlaue对设置到哈希表中,当key-field已经存在时,则放弃设置;否则,设置field-value:hsetnx key field value
hsetnx stu1001 age 30

6. redis中有关zset类型数据的操作命令:有序集合

跳跃表(跳表)

1.简介

? 有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等,但是数组不便与元素的插入与删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。

redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。

2.实例

? 对比有序链表和跳跃表,从链表中查询出51

(1) 有序链表

请添加图片描述

要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

(2) 跳跃表

Redis知识点总结_第3张图片

从第2层开始,1节点比51节点小,向后比较。

21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层,

在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下,

在第0层,51节点为要查找的节点,节点被找到,共查找4次。从此可以看出跳跃表比有序链表效率要高。

本质上是集合,所有元素不能重复;每一个元素都关联一个分数,redis会根据分数对元素进行自动排序;分数可以重复;既然有序集合中每一个元素都有顺序,那么也都有下标;有序集合中元素的排序规则又与列表中元素的排序规则不一样。 对学生的成绩进行排序

a) 将一个或者多个member及其score值加入有序集合:zadd key score member [score member ....] 如果元素已经存在,则把分数覆盖
zadd zset01 20 z1 30 z2 50 z3 40 z4

b) 获取指定有序集合中指定下标区间的元素:zrange key startIndex endIndex [withscores]
zrange zset01 0 -1 按成绩从小到大返回学生的名字
zrange zset01 0 -1 withscores 按成绩从小到大返回学生的名字加分数

c) 获取指定有序集合中指定分数区间(闭区间) 的元素:zrangebyscore key min max [withscores]
zrangebyscore zset01 30 50 withscores 获取成绩从30到50的学生的名字加分数

d) 删除指定有序集合中一个或者多个元素:zrem key member [member......]
zrem zset01 z3 z4

e) 获取指定有序集合中所有元素的个数:zcard key
zcard zset01

f) 获取指定有序集合中分数在指定区间内的元素的个数:zcount key min max
zcount zset01 20 50

g) 获取指定有序集合中指定元素的排名(按照分数从小到大的排名,排名从0开始) : zrank key member
zrank zset01 z4

h) 获取指定有序集合中指定元素的分数:zscore key member
zscore zset01 z4

i) 获取指定有序集合中指定元素的排名(按照分数从大到小的排名,排名从0开始) :zrevrank key membe
zrevrank zset01 z4

7. Bitmaps

现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图

Redis知识点总结_第4张图片
合理地使用操作位能够有效地提高内存使用率和开发效率。redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

(1) Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) ,但是它可以对字符串的位进行操作。

(2) Bitmaps单独提供了一套命令, 所以在redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

1. setbit

setbit设置Bitmaps中某个偏移量的值(0或1)offset:偏移量从0开始

例:

每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。

很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成redis的阻塞。

2. getbit

getbit获取Bitmaps中某个偏移量的值

例:

获取id=8的用户是否在2020-11-06这天访问过, 返回0说明没有访问过

3. bitcount

统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含。

bitcount 统计字符串从start字节到end字节比特值为1的数量

例:
计算2022-11-06这天的独立访问用户数量

8. 底层数据结构

Redis知识点总结_第5张图片

Redis 的底层数据结构有六种,简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组,String 的底层实现是简单动态字符串,List、Hash、Set 和 SortedSet 都有两种底层实现结构,这四种类型被称为集合类型,特点是一个 key 对应一个集合数据

跳跃表(跳表)

对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便于元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历,效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。

(1) 有序链表

在这里插入图片描述

要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

(2) 跳跃表:对于一个链表,每两个节点拉上一层。

Redis知识点总结_第6张图片

从第2层开始,1节点比51节点小,向后比较。21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层,在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下,在第0层,51节点为要查找的节点,节点被找到,共查找4次。从此可以看出跳跃表比有序链表效率要高。

压缩列表

Redis为了节约内存空间,zset和hash在对象比较少的时候,采用压缩列表(ziplist)来存储, 压缩列表支持双向遍历,ztail_offset字段是为了快速定位到最后一个元素,然后倒着遍历。

struct ziplist<T> {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}

Redis知识点总结_第7张图片

struct entry {
    int<var> prevlen; // 前一个 entry 的字节长度
    int<var> encoding; // 元素类型编码
    optional byte[] content; // 元素内容
}

prevlen字段是为了从后往前遍历的时候,通过这个字段快速定位到下个元素的位置,该字段是一个变长的整数,当字符串长度小于254时,使用一个字节表示;但大于254的时候采用5个字节表示,其中第一个字节是0xFF(254),剩下四个表示长度。encoding:元素内容存储编码格式

由于entries是紧凑存储,没有多余的空间来添加元素,所以每次添加的时候,系统都要进行扩容。 重新分配新的内存空间或者在原有地址上进行扩展,如果在原有地址上扩展就不需要进行旧内容的内存拷贝。

简单动态字符串

用于存储字符串和整型数据。SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。

struct sdshdr {
    int len;	//记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
    int free;	//记录buf数组中未使用字节的数量,当追加时,小于free就不需要内存重分配
    char buf[]; //字节数组,用于保存字符串
};

C字符串和SDS之间的比较

直接通过字段获取字符串长度,提高了效率
SDS有空间预分配,有一套自己的扩容机制,在这该机制下,减少了重新内存分配的次数
C字符串以"\0"结束,二进制不安全,因为存在特殊字符

SDS的free空间来自两种策略

空间预分配:若修改之后的len<1MB, 则分配 free = len;若修改之后的len>=1MB, 则分配free = 1MB

惰性空间释放:例如:sdstrim会对两端无用字符删减后,字符串整体位移到首部

哈希表

Redis 用一个哈希表保存所有键值对,实现 key-value 快速访问。一个哈希表就是一个数组,数组每个元素叫哈希桶,每个哈希桶保存键值对数据。然而哈希桶中的元素不是 value 本身,而是指向 value 的指针,即 value 存储的内存地址。

Redis知识点总结_第8张图片

如图,这个哈希表保存了所有键值对,哈希桶中的 entry 元素保存key 和value指针,哈希表能在 O(1) 时间复杂度快速查找键值对,所以只需要计算 key 的哈希值就能找到对应的哈希桶位置,进而找到对应的 entry 元素。不同类型的 value 都能被找到,不论是 String、List、Set、Hash。这种查找方式只需要进行一次哈希计算,不论数据规模多少,然而,在 Redis 中写入大量数据后,操作有时候会变慢,因为出现了哈希表的冲突以及 rehash 带来的操作阻塞。

哈希冲突

当哈希表中数据增加,新增的数据 key 哈希计算出的哈希值和老数据 key 的哈希值会在同一个哈希桶中,也就是说多个 key 对应同一个哈希桶。

链式哈希

Redis 中,同一个哈希桶中多个元素用一个链表保存,它们之间用指针连接,这就是链式哈希。如图所示,entry1、entry2 和 entry3 都保存在哈希桶 3 中,导致哈希冲突。entry1 增加个next 指针指向 entry2,entry2 增加next 指针指向 entry3,不论哈希桶 3 元素有多少个,都可以通过指针连接起来,形成一个链表,叫做哈希冲突链。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3zGDiVWW-1649604039564)(F:\markdown笔记\redis\642.png)]

链式哈希会产生一个问题,随着哈希表数据越来越多,哈希冲突越来越多,单个哈希桶链表上数据越来越多,查找时间复杂度退化到 O(n),查找耗时增加,效率降低。

rehash

为解决这个问题,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

Redis 使用两个全局哈希表:哈希表 1 和哈希表 2,最开始新增数据默认存到哈希表 1,哈希表 2 没有被分配空间,当数据增加,Redis 开始执行 Rehash 操作:

  1. 给哈希表 2 分配更大空间,可以是当前哈希表 1 大小的两倍
  2. 把哈希表 1 的数据重新映射并拷贝到哈希表 2
  3. 释放哈希表 1 空间

rehash 后,从哈希表 1 切换到哈希表 2,哈希表 2 空间更多,哈希冲突更少,原来哈希表 1 留做下次 rehash 扩容备用。在第二步涉及大量数据拷贝,如果一次性把哈希表 1 迁移完,耗时很长,会造成线程阻塞,无法处理其他请求,Redis 采用渐进式 rehash

渐进式 rehash

在第二步中,Redis 正常处理客户端请求,每处理一个请求,从哈希表 1 的第一个索引位置开始,把这个位置上的所有 entry 拷贝到哈希表 2 中。处理下一个请求时,把下一个索引位置的 entry 做同样操作。

Redis知识点总结_第9张图片

渐进式 rehash 把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

4. redis的配置文件

redis安装完成之后,在redis的根目录会提供一个配置文件(redis.windows.conf ) ,可以配置一些redis服务端运行时的一些参数。

redis服务可以参考配置文件中的参数进行运行,只有启动redis服务器指定使用的配置文件,参数才会生效,否则,redis会采用默认的参数运行。

1. redis配置文件中关于网络的配置

port:配置redis服务运行的端口号,如果不配置port,则redis服务默认使用6379端口。
bind:配置客户端连接redis服务时,所能使用的ip地址,默认可以使用redis服务器所在主机上任何一个ip。默认情况下,如果不配置bind,客户端连接redis服务时,通过服务器上任何一个ip都能连接到redis服务。一般情况下,bind都是配置服务器上某一个真实ip,为了安全。一旦配置了bind,客户端就只能通过bind指定的ip地址连接redis服务。

redis-cli.exe 连接127.0.0.1本机上的6379端口服务

一旦redis服务配置了port和bind(如果port不是6379、bind也不是127.0.0.1) ,客户端连接redis服务时,就要指定端口和ip:
redis-cli.exe -h bind绑定的ip地址 -p port设置的端口连接bind绑定的ip地址主机上的port设置的端口redis服务;

客户端关闭redis服务时:redis-cli.exe -h bind绑定的ip地址 -p port设置的端口 shutdown
redis-cli.exe -h 192.168.11.128 -p 6380 shutdown

tcp-keepalive:连接保活策略 ,由于客户端向服务端的连接是长连接,服务端每隔多久向客户端发起一次ACK请求 以检查客户端是否挂掉,如果挂了则会关闭连接。

2. 常规配置

loglevel:配置日志输出级别,开发阶段配置debug,上线阶段配置notice或者warning.
logfile:指定日志输出文件。redis在运行过程中,会输出一些日志信息;默认情况下,这些日志信息会输出到控制台;我们可以使用logfile配置日志文件,使redis把日志信息输出到指定文件中。
databases:配置redis服务默认创建的数据库实例个数,默认值是16。

3. 安全配置

requirepass:设置访问redis服务时所使用的密码;默认不使用。此参数必须在protected-mode=yes时才起作用。一旦设置了密码验证,客户端连接redis服务时,必须使用密码连接:redis-cli.exe -h ip -p port -a pwd

5. redis的持久化

redis是内存数据库,它把数据存储在内存中,这样在加快读取速度的同时也对数据安全性产生了新的问题,即当redis所在服务器发生宕机后,redis数据库里的所有数据将会全部丢失。

为了解决这个问题,redis提供了持久化功能――RDB和AOF(Append Only File),在适当的时机采用适当手段把内存中的数据持久化到磁盘中,每次redis服务启动时,都可以把磁盘上的数据再次加载内存中使用。

1. RDB策略

RDB策略是redis默认的持久化策略,redis服务开启时这种持久化策略就已经默认开启了。

**在指定时间间隔内,redis服务执行指定次数的写操作,会自动触发一次持久化操作,将内存中的数据写入到磁盘中。**即在指定目录下生成一个dump.rdb文件,redis重启会通过加载dump.rdb文件来恢复数据,RDB的缺点是最后一次持久化后的数据可能丢失。

RDB原理
redis会复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程,来进行持久化。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

RDB保存的文件
RDB保存的文件是dump.rdb文件,位置保存在redis的启动目录,每次同步数据到磁盘都会生成一个dump.rdb文件,新的dump.rdb会覆盖旧的dump.rdb文件。

配置RDB持久化策略

在.conf配置文件中配置RDB持久化策略:

  1. save
    配置复合的快照触发条件,即redis在seconds秒内key改变changes次,redis把快照内的数据保存到磁盘中一次。默认的策略是:1分钟内改变了1万次或者5分钟内改变了10次或者15分钟内改变了1次
    如果要禁用redis的持久化功能,则把所有的save配置都注释掉。

  2. stop-writes-on-bgsave-error:
    当bgsave快照操作出错时停止写数据到磁盘,这样能保证内存数据和磁盘数据的一致性,但如果不在乎这种一致性,要在bgsave快照操作出错时继续写操作,这里需要配置为no。

  3. rdbcompression:
    设置对于存储到磁盘中的快照是否进行压缩,设置为yes时,redis会采用LZF算法进行压缩;如果不想消耗CPU进行压缩的话,可以设置为no,关闭此功能。

  4. rdbchecksum:
    在存储快照以后,还可以让redis使用CRC64算法来进行数据校验,但这样会消耗一定的性能,如果系统比较在意性能的提升,可以设置为no,关闭此功能。

  5. dbfilename:
    redis持久化数据生成的文件名,默认是dump.rdb,也可以自己配置。

  6. dir:
    redis持久化数据生成文件保存的目录,默认是./即redis的启动目录,也可以自己配置。

在客户端执行FLUSHDB或者FLUSHALL或者SHUTDOWN时,也会把快照中的数据保存到dump.rdb,只不过这种操作已经把数据清空了,保存的也是空文件,没有意义。

手动保存RDB快照
save命令执行一个同步保存操作,将当前redis实例的所有数据快照(snapshot) 以RDB文件的形式保存到硬盘。由于save指令会阻塞所有客户端,所以保存数据库的任务通常由BGSAVE命令异步地执行,而save作为保存数据的最后手段来使用,当负责保存数据的后台子进程不幸出现问题时使用。

RDB数据恢复
通过脚本将redis产生的dump.rdb文件备份(cp dump.rdb dump_bak.rdb) ,每次启动redis前,把备份的dump.rdb文件替换到redis相应的目录(在redis.conf中配的的dir目录) 下,redis启动时会加载dump.rdb文件,并且把数据读到内存中。

RDB小结
redis默认开启RDB持久化方式,适合大规模的数据恢复,但它的数据一致性和完整性较差。

2. AOF策略

AOF(Append Only File) ,它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作(读操作不记录) ,并追加到文件中,只许追加文件但不可以改写文件。

redis 重启会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作,效率低下,redis默认不开启。

AOF保存的文件是appendonly.aof文件 ,位置保存在redis的启动目录。如果开启了AOF,redis每次记录写操作都会往appendonly.aof文件追加新的日志内容。往磁盘进行写操作

配置RDB持久化策略

在.conf配置文件中配置RDB持久化策略:

  1. appendonly:
    配置是否开启AOF,yes表示开启,no表示关闭。默认是no。
  2. appendfilename:
    AOF保存文件名
  3. appendfsync:
    AOF异步持久化策略
    always:同步持久化,每次发生数据变化会立刻写入到磁盘中。性能较差但数据完整性比较好(慢,安全)
    everysec:出厂默认推荐,每秒异步记录一次(默认值)
    no:不即时同步,由操作系统决定何时同步。
  4. no-appendfsync-on-rewrite:
    重写时是否可以运用appendsync,默认no,可以保证数据的安全性。

AOF数据恢复
通过脚本将redis产生的appendonly.aof文件备份(cp appendonly.aof appendonly_bak.aof) ,每次启动redis前,把备份的appendonly.aof文件替换到redis相应的目录(在redis.conf中配的的dir目录) 下,只要开启AOF的功能,redis每次启动,会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

但在实际开发中,可能因为某些原因导致appendonly.aof 文件格式异常,从而导致数据还原失败,可以通过命令redis-check-aof --fix appendonly.aof 进行修复,此命令会把出现异常的部分往后所有写操作日志去掉。

AOF的重写
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。

AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename) 。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定redis要满足一定条件才会进行重写。 redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次重写后大小的一倍且文件大于64M时触发。当然,也可以在配置文件中进行配置。

重写流程
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)
 (1) 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
 (2) 主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

小结:

redis需要手动开启AOF持久化方式,AOF的数据完整性比RDB高,但记录内容多了,会影响数据恢复的效率。

关于redis持久化的使用:若只打算用redis做缓存,可以关闭持久化。若打算使用redis的持久化,建议RDB和AOF都开启。其实RDB更适合做数据的备份,留一后手。AOF出问题了,还有RDB。AOF与RDB模式可以同时启用,这并不冲突。如果AOF是可用的,那redis启动时将自动加载AOF,这个文件能够提供更好的持久性保障。

小结:根据数据的特点决定开启哪种持久化策略;一般情况,开启RDB足够了。官方推荐两个都启用。如果对数据不敏感,可以选单独用RDB。不建议单独用 AOF,因为可能会出现Bug。如果只是做纯内存缓存,可以都不用。

6. redis的事务

数据库的事务:把一组数据库命令放在一起执行,保证操作原子性,要么同时成功,要么同时失败。

redis的事务:允许把一组redis命令放在一起,将命令进行序列化,然后一起执行,保证部分原子性。

1. redis的事务提交与执行

1. multi——用来标记一个事务的开始。

redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列。
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

2. exec——用来执行事务队列中所有的命令。
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
返回值:这个命令的返回值是一个数组,其中的每个元素分别是原子化事务中的每个命令的返回值。

例如:
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。

multi
set k3 v3
seta kk vv// 有误,组队中出错
set k4 v4
exec

Redis知识点总结_第10张图片

例如:

执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

multi
set k3 v3
incr k3 // 队列提交后,执行时出错
set k4 v4
exec

Redis知识点总结_第11张图片

2. redis的事务的结束

discard:清除所有已经压入队列中的命令,并且结束整个事务。

multi
set k5 v5 
set k6 v6
discard

3. redis的事务的监控

1. watch:监控某一个键,当事务在执行过程中,此键代码的值发生变化,则本事务放弃执行,否则,正常执行。

当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的,如果被监控的key值在本事务外有修改时,则本事务所有指令都不会被执行,Watch命令相当于关系型数据库中的乐观锁。

加一个字段 每次改之前先查version,修改时条件为version和之前相同 并且修改余额加version
不,重新查 重新赋值

update table set balance - balance-dept,version = version + 1 where id = xxxx and version = 100

set balance 100 // 初始化数据
set balance2 1000
watch balance // 监控balance
multi // 开启事务
decrby balance 50 //balance 减50
incrby balance2 50 //balance2 加50
exec // 执行事务,如果在此期间balance被别人修改 则放弃执行事务

2. unwatch:放弃监控所有的键。
清除所有先前为一个事务监控的键。如果在watch命令之后你调用了EXEC或DISCARD命令,那么就不需要手动调用UNWATCH命令。

watch version
unwach // 取消监控所有key
multi
decrby balance 50
incrby balance2 50
exec

应用:
银行卡余额先再删除,若同时有多个用户需要修改同一个账户,就需要加锁

4. 事务总结

redis事务三特性
单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 。

悲观锁(Pessimistic Lock) , 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock) , 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。redis就是利用这种check-and-set机制实现事务的。

7. redis消息的发布与订阅

redis 发布订阅(pub/sub) 是一种消息通信模式:发送者(pub) 发送消息,订阅者(sub) 接收消息。
redis 客户端可以订阅任意数量的频道,消息的发布者往频道上发布消息,所有订阅此频道的客户端都能够接受到消息。

客户端间消息的通信

  1. subscribe:订阅一个或者多个频道的消息。返回值:订阅的消息
    subscribe ch1 ch2 ch3
  1. publish:将消息发布到指定频道。返回值:数字。接收到消息订阅者的数量。
    publish ch1 hello

  2. psubcribe:订阅一个或多个符合给定模式的频道。模式以 * 作为通配符,例如:news.* 匹配所有以 news、开头的频道。返回值:订阅的信息。
    psubscribe news.*
    例:
    Redis知识点总结_第12张图片

8. 实现redis的集群高可用:6个999.9999% 全年停机不超过32秒

从redis 3.0之后版本支持redis-cluster集群,至少需要3(Master) +3(Slave) 才能建立集群。

redis cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

  1. 所有的redis节点彼此互联(PING-PONG机制) ,内部使用二进制协议优化传输速度和带宽。

  2. 节点的fail是集群中超过半数的节点检测失效时才生效。

  3. 客户端与redis节点直连,不需要中间proxy层.,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

  4. redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护

  5. redis集群预分好16384个哈希槽,当需要在 redis 集群中放置一个 key-value 时, redis 先对key 使用crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。

关于为什么是16384个槽位的原因如下:
key经过CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值,换句话说,值分布在0~65535之间。当节点彼此间进行通信是会互相交换数据信息,信息由消息头和消息体组成,消息头中最占空间的是一个叫myslots的数组,长度为CLUSTER_SLOTS/8,也就是槽数/8,不难知道槽数越大,消息头越大,则节点通信会更占带宽。

其次还有两个原因,redis的集群主节点数量基本不可能超过1000个,16384个槽位足够了。以及Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,哈希槽数量越多,bitmap的压缩率越低。

详细内容见:https://www.cnblogs.com/rjzheng/p/11430592.html

1. 搭建一主二从模拟redis集群

1. 搭建三台redis服务

使用一个redis模拟三台redis服务
提供三份redis配置文件:redis6379.conf. redis6380.conf. redis6381.conf
修改三份配置文件:以redis6379.conf为例,改的内容

bind 127.0.0.1
port 6379
logfile "6379.log"
dbfilename dump6379.rdb

分别使用三个redis配置文件,启动三个redis服务

2. 通过redis客户端分别连接三台redis服务

redis-cli.exe -h 127.0.0.1 -p 6379
redis-cli.exe -h 127.0.0.1 -p 6380
redis-cli.exe -h 127.0.0.1 -p 6381

3. 查看三台redis服务在集群中的主从角色:
info replication
默认情况下,所有的redis服务都是主机,即都能写和读,但是都还没有从机。

4. 先在6379进行写操作:
set k1 v1
三台redis服务互相独立,互不影响,另外两个无k1 v1。

5. 设置主从关系:设从不设主 6379为主机

在6380上执行:slaveof 127.0.0.1 6379
在6381上执行:slaveof 127.0.0.1 6379

查看6380和6381服务的主从角色:info replication。此时6380 和6381已经有主机上已有的数据k1 v1了,此时为全量复制。

6. 全量复制:一旦主从关系确定,会自动把主库上已有的数据同步复制到从库

在6380和6381上执行:keys *

7. 增量复制:主库写数据会自动同步到从库

在6379上执行:set k2 v2
在6380和6381上执行:keys * 自动复制

8. 主写从读,读写分离

在6380和6381上执行:set k3 v3 ===报错 因为从只能读

9. 主机宕机. 从机原地待命

关闭6379服务:redis-cli.exe -h 127.0.0.1 -p 6379 shutdown

从机角色还是从属于6379的从机 只不过连接状态变为了down,不影响读只不过数据不进行更新了

10. 主机恢复. 一切恢复正常

重启6379服务:redis-server

客户端连接6379:redis-cli -h 127.0.0.1 -p 6379

从机角色还是从属于6379的从机 只不过连接状态变为了up

11. 从机宕机. 主机少一个从机. 其它从机不变

关闭6380服务: redis-cli.exe -h 127.0.0.1 -p 6380 shutdown

查看6379服务的主从角色:info replication 发现从机数量减少一个

12. 从机恢复. 需要重新设置主从关系

重启6380服务:redis-server.exe redis6380.conf &
客户端连接6380:redis-cli.exe -h 127.0.0.1 -p 6380 此时变为主机 需要重新设置,在6380上执行: slaveof 127.0.0.1 6379

13. 从机上位: 主机出现问题时,从机代替主机

a. 主机宕机,从机原地待命:

关闭6379服务:redis-cli.exe -h 127.0.0.1 -p 6379 shutdown

b. 从机断开原来主从关系:

在6380上执行:slaveof no one
查看6380服务的主从角色:info replication 此时6380变为主机

c. 重新设置主从关系

在6381上执行:slaveof 127.0.0.1 6380

d. 之前主机恢复,变成孤家寡人: 后面可以做主机也可以做从机

重启6379服务:redis-server.exe redis6379.conf &
客户端连接6379:redis-cli.exe -h 127.0.0.1 -p 6379

e. 天堂变地狱:让他先做从机

在6379上执行:slaveof 127.0.0.1 6381
在6381上执行:info replication 既是主机又是从机 6379的主机 6380的从机

Redis知识点总结_第13张图片

1、一台主机配置多台从机,一台从机又可以配置多台从机,从而形成一个庞大的集群架构。减轻了一
台主机的压力,但是增加了服务间的延迟时间。

2、redis的主从复制最大的缺点就是延迟,主机负责写,从机负责备份,这个过程有一定的延迟,当系
统很繁忙的时候,延迟问题会更加严重,从机器数量的增加也会使这个问题更加严重。

3、服务器的运行ID(run ID):每个 Redis 服务器在运行期间都有自己的 run ID , run ID 在服务器启
动的时候自动生成。从服务器会记录主服务器的 run ID ,这样如果发生断网重连,就能判断新连接上
的主服务器是不是上次的那一个,这样来决定是否进行数据部分重传还是完整重新同步。

4、Slave启动成功连接到master后会发送一个sync命令,Master接到命令启动后台的存盘进程,同时
收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到
slave,以完成一次完全同步。

5、复制偏移量 offset:主服务器和从服务器都会维护一个复制偏移量,主服务器每次向从服务器中传
递 N 个字节的时候,会将自己的复制偏移量加上 N。从服务器中收到主服务器的 N 个字节的数据,就
会将自己额复制偏移量加上 N。通过主从服务器的偏移量对比可以很清楚的知道主从服务器的数据是否
处于一致。如果不一致就需要进行增量同步了

2. 相关概念

高并发
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS (Query Per Second),并发用户数等。

响应时间
系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是 系统的响应时间。

吞吐量
单位时间内处理的请求数量。

QPS
每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。

并发用户数
同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在一定程度上代表了系统的并发用户数。

提升系统的并发能力
提高系统并发能力的方式,方法论上主要有两种:垂直扩展(Scale Up)与水平扩展(Scale Out)。

3. 全量复制

slave启动成功连接到master后会发送一个sync命令,master接到命令启动后台的存盘进程,同时收集所有接收到用于修改数据集的命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步。

slave服务在接收到数据库文件数据后,将其存盘并加载到内存中,只要是重新连接master,一次完全同步(全量复制) 将被自动执行。

也就是一旦主从关系确定,会自动把主库上已有的数据同步复制到从库。

4. 增量复制

Master将新的所有收集到的修改命令依次传给slave,完成同步。也就是当slave成功连接master,并且自动一次完全同步(全量复制) 后剩下的都是增量复制。

5. 垂直扩展

提升单机处理能力,垂直扩展的方式又有两种:

  1. 增强单机硬件性能,例如:增加CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G;

  2. 提升单机架构性能,例如:使用Cache来减少IO次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间;

总结:不管是提升单机硬件性能,还是提升单机架构性能,都有一个致命的不足:单机性能总是有极限的。所以互联网分布式架构设计高并发终极解决方案还是水平扩展。

6. 水平扩展

只要增加服务器数量,就能线性扩充系统性能。水平扩展对系统架构设计是有要求的,难点在于:如何在架构各层进行可水平扩展的设计

9. redis哨兵机制:主机宕机,从机上位的自动版。

Redis 中集群的高可用方式,哨兵节点是特殊的 Redis 服务,不提供读写,主要来监控 Redis 中的实例节点,如果监控服务的主服务器下线了,会从所属的从服务器中重新选出一个主服务器,代替原来的主服务器提供服务,主机宕机,从机上位的自动版。

核心功能就是:监控,选主,通知。

**监控:**哨兵机制,会周期性的给所有主服务器发出 PING 命令,检测它们是否仍然在线运行,如果在规
定的时间内响应了 PING 通知则认为,仍在线运行;如果没有及时回复,则认为服务已经下线了,就会
进行切换主库的动作。

**选主:**当主库挂掉的时候,会从从库中按照既定的规则选出一个新的的主库

**通知:**当一个主库被新选出来,会通知其他从库,进行连接,然后进行数据的复制。当客户端试图连接失效的主库时,集群也会向客户端返回新主库的地址,使得集群可以使用新的主库。

1. 什么时候整个集群不可用(cluster_state:fail)

  1. 如果集群任意master挂掉,且当前master没有slave,集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完整时进入fail状态。

  2. 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。

1. 搭建一主二从集群架构

1. 搭建三台redis服务

使用一个redis模拟三台redis服务
提供三份redis配置文件:redis6379.conf、redis6380.conf、redis6381.conf
修改三份配置文件:以redis6379.conf为例,改的内容如下:

bind 127.0.0.1
port 6379
logfile "6379.log"
dbfilename dump6379.rdb

分别使用三个redis配置文件,启动三个redis服务

2. 通过redis客户端分别连接三台redis服务
redis-cli.exe -h 127.0.0.1 -p 6379
redis-cli.exe -h 127.0.0.1 -p 6380
redis-cli.exe -h 127.0.0.1 -p 6381

3. 查看三台redis服务在集群中的主从角色
info replication
默认情况下,所有的redis服务都是主机,即都能写和读,但是都还没有从机。

4. 先在6379进行写操作
set k1 v1
三台redis服务互相独立,互不影响,另外两个无k1 v1。

5. 设置主从关系:设从不设主 6379为主机

在6380上执行:slaveof 127.0.0.1 6379
在6381上执行:slaveof 127.0.0.1 6379

查看6380和6381服务的主从角色:info replication。此时6380 和6381已经有主机上已有的数据k1 v1了,此时为全量复制。

6. 提供哨兵配置文件

在redis安装目下创建配置文件:redis_sentinel.conf,并编辑里边的内容:sentine- monitor dc-redis 127.0.0.1 6379 1,指定监控主机的ip地址,port端口,得到哨兵的投票数(当哨兵投票数大于或者等于此数时切换主从关系)

7. 启动哨兵服务

redis-sentine -redis_sentinel.conf

8. 主机宕机

关闭6379服务:redis-cli.exe -h 127.0.0.1 -p 6379 shutdown

哨兵程序自动选择从机上位。

9. 之前主机恢复:自动从属于新的主机

重启6379服务:redis-server.exe redis6379.conf
客户端连接6379:redis-cli.exe -h 127.0.0.1 -p 6379

2. 选主的准确性

哨兵会通过 PING 命令检测它和从库,主库之间的连接情况,如果发现响应超时就会认为给服务已经下线了。

如果集群的网络压力比较大,网路堵塞,这时候会存在误判的情况。如果误判的节点是从节点,影响不会很大,拿掉一个从节点,对整体的服务,影响不大,还是会不间断的对外提供服务。

如果误判的节点是主节点,影响就很大了,主节点被标注下线了,就会触发后续的选主,数据同步,等一连串的动作,这一连串的动作很很消耗性能的。所以对于误判,应该去规避。

3. 如何减少误判呢?

引入哨兵集群,一个哨兵节点可能会进行误判,引入多个少哨兵节点一起做决策,就能减少误判了。

当有多个哨兵节点的时候,大多数哨兵节点认为主库下线了,主库才会真正的被标记为下线了,一般来讲当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库下线了,才能最终判定主库的下线状态,当然这个数值在 Redis 中是可以配置的。

4. 如何选主?

  1. 过滤掉已经下线的服务器;
  2. 过滤掉最近5秒钟没有回复过主节点的 INFO(用于观察服务器的角色) 命令的服务器,保证选中的服务器都是最近成功通过信的;
  3. 过滤掉和下线主服务器连接超过 down-after-milliseconds*10 毫秒的从服务器, down- after-milliseconds 是主服务器下线的时间,这一操作避免从服务器与主服务器过早的断开,影响到从库中数据同步,因为断开时间越久,从库里面的数据就越老旧过时。

然后对这些服务器根据 slave-priority 优先级(这个优先级是手动设置的,比如希望哪个从服务器优先变成主服务器,优先级就设置的高一点) 进行排序。

如果几台从服务器优先级相同,然后根据复制偏移量从大到小进行排序,如果还有相同偏移量的从服务器,然后按照 runID 从小到大进行排序,直到选出一台从服务器。

5. 哨兵集群的主节点选举

在哨兵集群中也是有一个 Leader 节点的,当一个从库被选举出来,从库的切换是由 Leader 节点完成的。哨兵Leader 节点的选举总结起来就是:

  1. 每个做主观下线的哨兵节点向其他哨兵节点发送命令,要求将自己设置为领导者;
  2. 接收的哨兵可以同意或者拒绝;
  3. 如果该哨兵节点发现自己的票数已经超过半数并且超过了规定的节点数,规定的节点数用来配置判断主节点宕机的哨兵节点数。
  4. 如果此过程选举出了多个领导者,那么将等待一段时重新进行选举;

6. 故障转移

  1. 哨兵的领导者从从机中选举出合适的丛机进行故障转移;
  2. 对选取的从节点进行 slave of no one 命令,(这个命令用来让从机关闭复制功能,并从从机变为主机);
  3. 更新应用程序端的链接到新的主节点;
  4. 对其他从节点变更 master 为新的节点;
  5. 修复原来的 master 并将其设置为新的 master 的从机。

7. 消息通知

哨兵和哨兵之前,哨兵和从库之间,哨兵和客户端是如何相互发现,进行消息传递?

哨兵和哨兵之间的相互发现,通过 Redis 提供的 pub/sub 机制实现,因为每个哨兵节点都会和主库进行连接,通过在主库中发布信息,订阅信息,就能找到其他实例的连接信息。

哨兵节点和从库,通过哨兵向主库发送 INFO 命令来完成,哨兵给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。

哨兵和客户端之间:每个哨兵实例也提供 pub/sub 机制,客户端可以从哨兵订阅消息,来获知主从库切换过程中的不同关键事件。

哨兵提升一个从库为新主库后,哨兵会把新主库的地址写入自己实例的 pubsub(switch-master)中。客户端需要订阅这 个pubsub,当这个 pubsub 有数据时,客户端就能感知到主库发生变更,同时可以拿到最新的主库地址,然后把写请求写到这个新主库即可,这种机制属于哨兵主动通知客户端。

如果客户端因为某些原因错过了哨兵的通知,或者哨兵通知后客户端处理失败了,安全起见,客户端也
需要支持主动去获取最新主从的地址进行访问。

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

redis 集群实现了对redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。

redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。

10. redis缓存与数据库一致性

实时同步

对强一致要求比较高的,应采用实时同步方案,即查询缓存查询不到再从DB查询,保存到缓存;更新缓存时,先更新数据库,再将缓存的设置过期(建议不要去更新缓存内容,直接设置缓存过期) 。

数据库与缓存不一致如何解决

不一致产生的原因

我们在使用redis过程中,通常会这样做:先读取缓存,如果缓存中数据不存在,则读取数据库,之后再将读取到的数据写入缓存。

不管是先写库,再删除缓存,还是先删缓存,再写库,都有可能出现数据不一致的情况,因为写和读是并发的,没法保证顺序。

先删缓存再写库

删完缓存还没来得及写进主库前,或是写进主库但是未同步至从库,另一个线程就来读取,发现缓存为空,则会去从库读取数据,此时读到的数据为脏数据,并且还会在读取后会将这条脏数据写入缓存。

一个先删缓存再写库的例子:
Redis知识点总结_第14张图片

(1+2)主库写入数据,删除缓存中的数据。
(3+4+5)接着立刻一个读请求,先读缓存,未命中缓存,之后从库,读到的数据写入缓存,以便后续的读能够cache hit(但是此时的主从同步还没有完成,缓存中放入了旧数据)
(6)最后,主从同步完成

也就是线程删除缓存中的数据后将此条数据写入主库,写入到主库的数据还未同步至从库,当用户去读取此条数据时会先去缓存中读取,缓存中没有(已经被线程删除),再去从库中读。但由于主库中的数据库还未同步至从库,所以导致读到了旧数据,之后又将此读到的数据写入了缓存中。

先写库再删缓存

(这种方式使用相对较多)写完库还未删除缓存前,另一个线程就来读取,则会读到那条本该从缓存中删除的数据,则也会出现数据不一致情况。 如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。

解决办法

首先,可以通过工具订阅从库的binlog,获取到从主库写完数据到从库写完数据的时间,淘汰这段时间内可能写入缓存的旧数据。如此这般,至少能够保证引入缓存之后,主从不一致,不会比没有引入缓存更坏,不过还是可能在写入主库前读取到脏数据。

设置缓存过期时间,所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值,然后再写入缓存中;

双删,其实就是删两次缓存的意思。延时,指的是在第一次删完缓存后,延迟一段时间,比如5秒或其他时间,然后再进行第二次删除缓存。

异步队列

用户进行高并发操作(读写)走redis,MYSQL进行数据异步处理。 例如进行定时任务: 每天凌晨1点将redis中数据取得,更新到MYSQL。

或者使用消息队列: RabbitMQ RocketMQ Kafka ,作用: 异步、流量销峰。

11. redis 单线程

redis单线程

在一开始的时候,Redis采用的是单线程模型, Redis 是基于内存操作的,它的瓶颈在于机器的内存、网络带宽,而不是 CPU,因为在CPU 还没达到瓶颈时内存可能就先满了、或者带宽达到瓶颈了。因此 CPU 不是主要原因,那么自然就采用单线程了。更何况使用多线程还会面临一些额外的问题,比如共享资源的保护等等,对于一个 CPU 不是主要瓶颈的键值对数据库而言,采用单线程是非常合适的。

单线程为什么速度快?

  • 基于内存操作:Redis 的所有数据都在内存中,因此所有的运算都是内存级别的,所以它的性能比较高
  • 数据结构简单:Redis 的数据结构是为自身专门量身打造的,而这些数据结构的查找和操作的时间复杂度都是 O(1)
  • 多路复用和非阻塞 I/O:Redis 使用 I/O 多路复用功能来监听多个 socket 连接客户端,这样就可以使用一个线程来处理多个情况,从而减少线程切换带来的开销,同时也避免了 I/O 阻塞操作,从而大大地提高了 Redis 的性能
  • 避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的开销,而且单线程不会导致死锁的问题发生

非阻塞 I/O 和 I/O 多路复用

get name 对于 Redis 服务端而言,都发生了哪些事情呢?

服务端必须要先监听客户端请求(bind/listen),然后当客户端到来时与其建立连接(accept),从 socket 中读取客户端的请求(recv),对请求进行解析(parse),这里解析出的请求类型是 get、key 是 “name”,再根据 key 获取对应 value,最后返回给客户端,也就是向 socket 写入数据(send)。

以上所有操作都是由 Redis 主线程依次执行的,但是如果是阻塞IO里面会有潜在的阻塞点,分别是 accept和 recv。当 Redis 监听到一个客户端有连接请求、但却一直未能成功建立连接,那么主线程会一直阻塞在 accept 函数这里,导致其它客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv 从客户端读取数据时,如果数据一直没有到达,那么 Redis 主线程也会一直阻塞在 recv 这一步,因此这就导致了效率会变得低下,以上都是阻塞 I/O会面临的情况

非阻塞 I/O

但很明显,Redis 不会允许这种情况发生,因为,Redis 采用的是非阻塞 I/O,也就是将 socket 设置成了非阻塞模式。

在 socket 模型中,调用 socket() 方法会返回 “主动套接字”,调用 bind() 方法绑定 IP 和 端口,再调用 listen() 方法将 “主动套接字” 转化为 “监听套接字”,最后 “监听套接字” 调用 accept() 方法等待客户端连接的到来,当和客户端建立连接时再返回 “已连接套接字”,而后续就通过 “已连接套接字” 来和客户端进行数据的接收与发送。

在 listen() 这一步,会将 “主动套接字” 转化为 “监听套接字”,而此时的 “监听套接字” 的类型是阻塞的,阻塞类型的 “监听套接字” 在调用 accept() 方法时,如果没有客户端来连接的话,就会一直处于阻塞状态,那么此时主线程就没法干其它事情了。所以在 listen() 的时候可以将其设置为非阻塞,而非阻塞的 “监听套接字” 在调用 accept() 时,如果没有客户端连接请求到达时,那么主线程就不会傻傻地等待了,而是会直接返回,然后去做其它的事情。

同样,在创建 “已连接套接字” 的时候也可以将其类型设置为非阻塞,因为阻塞类型的 “已连接套接字” 在调用 send() / recv() 的时候也会处于阻塞状态,比如当客户端一直不发数据的时候,“已连接套接字” 就会一直阻塞在 rev() 这一步。如果是非阻塞类型的 “已连接套接字”,那么当调用 recv() 但却收不到数据时,也不用处于阻塞状态,同样可以直接返回去做其它事情。
Redis知识点总结_第15张图片

但是有两点需要注意:

  • 虽然 accept() 不阻塞了,在没有客户端连接时 Redis 主线程可以去做其它事情,但如果后续有客户端连接,Redis 要如何得知呢?因此必须要有一种机制,能够继续在 “监听套接字” 上等待后续连接请求,并在请求到来时通知 Redis。
  • send() / recv() 不阻塞了,相当于 I/O 的读写流程不再是阻塞的,读写方法都会瞬间完成并且返回,也就是它会采用能读多少就读多少、能写多少就写多少的策略来执行 I/O 操作,这显然更符合我们对性能的追求。但这样会面临一个问题,那就是当我们执行读取操作时,有可能只读取了一部分数据,剩余的数据客户端还没发过来,那么这些这些数据何时可读呢?同理写数据也是这种情况,当缓冲区满了,而我们的数据还没有写完,那么剩下的数据又何时可写呢?因此同样要有一种机制,能够在 Redis 主线程做别的事情的时候继续监听 “已连接套接字”,并且有数据可读写的时候通知 Redis。

这样才能保证 Redis 线程既不会像基本 IO 模型中一直在阻塞点等待,也不会无法处理实际到达的客户端连接请求和可读写的数据,而上面所提到的机制便是 I/O 多路复用。I/O 多路复用机制是指一个线程处理多个 IO 流,Linux 采用的是 epoll。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求,一旦有请求到达就会交给 Redis 线程处理,这样就实现了一个 Redis 线程处理多个 IO 流的效果。

Redis知识点总结_第16张图片

上图就是基于多路复用的 Redis IO 模型,图中的 FD 就是套接字,可以是 “监听套接字”、也可以是 “已连接套接字”,Redis 会通过 epoll 机制来让内核帮忙监听这些套接字。而此时 Redis 线程或者说主线程,不会阻塞在某一个特定的套接字上,也就是说不会阻塞在某一个特定的客户端请求处理上。因此 Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。但为了在请求到达时能够通知 Redis 线程,epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

那么回调机制是怎么工作的呢?以上图为例,首先 epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。这些事件会被放进一个队列中,Redis 主线程会对该事件队列不断进行处理,这样一来 Redis 就无需一直轮询是否有请求发生,从而避免资源的浪费。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。

我们以实际的连接请求和数据读取请求为例,再解释一下。连接请求和数据读取请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数,当 Linux 内核监听到有连接请求或数据读取请求时,就会触发 Accept 事件或 Read 事件,然后 内核就会回调 Redis 相应的 accept 和 get 函数进行处理。 等待Redis 主线程对该事件队列处理

Redis 6.0多线程

Redis 6.0 引入了一些新特性,其中非常受关注的一个特性就是多线程。在 4.0 之前 Redis 是单线程的,因为单线程的优点很明显,不但降低了 Redis 内部实现的复杂性,也让所有操作都可以在无锁的情况下进行,并且不存在死锁和线程切换带来的性能以及时间上的消耗。但是其缺点也很明显,单线程机制导致 Redis 的 QPS(Query Per Second,每秒查询数)很难得到有效的提高。

Redis 在 4.0 以及之后的版本中引入了惰性删除/异步删除,这是由额外的线程执行的,意思就是可以使用异步的方式对 Redis 中的数据执行删除操作了,这样处理的好处就是不会使 Redis 的主线程卡顿,会把这些删除操作交给后台线程来执行。例如:unlink keyflushdb asyncflushall async,同步的话则是 del key

通常情况下使用 del 指令可以很快的删除数据,但是当被删除的 key 是一个非常大的对象时,例如:删除的是包含了成千上万个元素的 hash 集合,那么 del 指令就会造成 Redis 主线程卡顿,因此使用惰性删除可以有效的避免 Redis 卡顿的问题。

除了惰性删除,像持久化、集群数据同步等等,都是由额外的子线程执行的,而 Redis 主线程则专注于网络 IO 和键值对读写。

Redis 6.0 中的多线程则是真正为了提高 I/O 的读写性能而引入的,它的主要实现思路是将主线程的 I/O 读写任务拆分给一组独立的子线程去执行,也就是说从 socket 中读数据和写数据不再由主线程负责,而是交给了多个子线程,这样就可以使多个 socket 的读写并行化了。这么做的原因就在于,虽然在 Redis 中使用了 I/O 多路复用和非阻塞 I/O,但我们知道数据在内核态空间和用户态空间之间的拷贝是无法避免的,而数据的拷贝这一步是阻塞的,并且当数据量越大时拷贝所需要的时间就越多。所以 Redis 在 6.0 引入了多线程,用于分摊同步读写 I/O 压力,从而提升 Redis 的 QPS。但是注意:Redis 的命令本身依旧是由 Redis 主线程串行执行的,只不过具体的读写操作交给独立的子线程去执行了(后面会详细说明 Redis 的主线程和子线程之间是如何协同的),而这么做的好处就是不需要为 Lua 脚本、事务的原子性而额外开发多线程互斥机制,这样一来 Redis 的线程模型实现起来就简单多了。因为和之前一样,所有的命令依旧是由主线程串行执行的,只不过具体的读写任务交给了子线程。

主线程和子线程的协同

整体可以分为四个阶段:

阶段一:服务端和客户端建立 socket 连接,并分配子线程(处理线程)

首先,主线程负责接收建立连接请求,当有客户端请求到达时,主线程会创建和客户端的 socket 连接,将该 socket 放入到全局等待队列中,然后通过轮询的方式选择子线程,并将队列中的 socket 连接分配给它,所以无论是从客户端读数据还是向客户端写数据,都由子线程来做。因为我们说 Redis 6.0 中引入多线程就是为了缓解主线程的 I/O 读写压力,而 I/O 读写这一步是阻塞的,所以应该交给子线程并行操作。

阶段二:子线程读取并解析请求

主线程一旦把 socket 连接分配给子线程,那么会进行阻塞状态,等待子线程完成客户端请求的读取和解析,得到具体的命令操作。由于可以有多个子线程,所以这个操作很快就能完成。

阶段三:主线程执行命令操作

等到子线程读取到客户端请求并解析完毕之后,然后再由主线程以单线程的方式执行命令操作,I/O 读写虽然交给了子线程,但是命令本身还是由 Redis 主线程执行的。

阶段四:子线程回写 socket、主线程清空全局队列

当主线程执行完命令操作时,还需要将结果写入缓冲区,而这一步显然要由子线程来做,因为是 I/O 读写。此时主线程会陷入阻塞,直到子线程将这些结果写回 socket 并返回给客户端。和读取一样,子线程将数据写回 socket 时,也是有多个线程在并行执行,所以写回 socket 的速度也很快。之后主线程会清空全局队列,等待客户端的后续请求。

Redis知识点总结_第17张图片

开启多线程

修改 redis.conf 中的两个配置。

1. 设置 io-thread-do-reads 配置项为 yes,表示启用多线程。

io-thread-do-reads yes

2. 通过 io-threads 设置子线程的数量。

io-threads 3

表示开启 3 个子线程,但是注意,线程数要小于机器的 CPU 核数,线程数并不是越大越好。关于线程数的设置,官方的建议是如果为 4 核的 CPU,那么设置子线程数为 2 或 3;如果为 8 核的CPU,那么设置子线程数为 6。

Redis6.0 引入的多线程 I/O 特性对性能的提升至少是一倍以上,其核心处理是单线程的,所以线程安全。

遍历顺序

我们的Redis中有4个key,我们每次只遍历一个一维数组中的元素。SCAN命令的遍历顺序是0->2->1->3这个顺序看起来有些奇怪。我们把它转换成二进制就好理解一些了。00->10->01->11我们发现每次这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的。(左低位右高位)。

这是因为需要考虑遍历时发生字典扩容与缩容的情况。我们来看一下在SCAN遍历过程中,发生扩容时,遍历会如何进行。加入我们原始的数组有4个元素,也就是索引有两位,这时需要把它扩充成3位,并进行rehash。原来挂接在xx下的所有元素被分配到0xx和1xx下。当我们即将遍历10时,dict进行了rehash,这时,scan命令知道自己应该从10开始遍历,所以会从010开始遍历,而000和100(原00下挂接的元素)不会再被重复遍历。

再来看看缩容的情况。假设dict从3位缩容到2位,当即将遍历110时,dict发生了缩容,这时scan会遍历10。这时010下挂接的元素会被重复遍历,但010之前的元素都不会被重复遍历了。所以,缩容时还是可能会有些重复元素出现的。

持久化原理

redis提供了持久化功能――RDB和AOF(Append Only File),在适当的时机采用适当手段把内存中的数据持久化到磁盘中,每次redis服务启动时,都可以把磁盘上的数据再次加载内存中使用。

12. redis删除过期key

redis 的 key 清理,也就是内存回收的时候主要分为:过期删除策略内存淘汰策略两部分。

过期删除策略

定时检查删除

对于每一个设置了过期时间的 key 都会创建一个定时器,一旦达到过期时间都会删除。这种方式立即清除过期数据,对内存比较好,但是有缺点是:占用了大量 CPU 的资源去处理过期数据,会影响 redis 的吞吐量 和 响应时间。

惰性检查删除

当访问一个 key 的时候,才会判断该 key 是否过期,如果过期就删除。该方式能最大限度节省 CPU 的资源。但是对内存不太好,有一种比较极端的情况:出现大量的过期 key 没有被再次访问,因为不会被清除,导致占用了大量的内存。

定期检查删除

每隔一段时间,扫描redis 中过期key 的字典,并清除部分过期的key。这种方式是前俩种一种折中方法。不同的情况下,调整定时扫描时间间隔,让CPU 与 内存达到最优。

内存淘汰策略

redis 内存淘汰策略是指达到maxmemory极限时,使用某种算法来决定来清理哪些数据,以保证新数据存入。

不处理,等报错(默认的配置)

noeviction,发现内存不够时,不删除key,执行写入命令时直接返回错误信息。(Redis默认的配置就是noeviction)

从所有结果集中的key中挑选,进行淘汰

allkeys-random 就是从所有的key中随机挑选key,进行淘汰

allkeys-lru 就是从所有的key中挑选最近使用时间距离现在最远的key,进行淘汰

allkeys-lfu 就是从所有的key中挑选使用频率最低的key,进行淘汰。(这是Redis 4.0版本后新增的策略)

从设置了过期时间的key中挑选,进行淘汰

这种就是从设置了expires过期时间的结果集中选出一部分key淘汰,挑选的算法有:

volatile-random 从设置了过期时间的结果集中随机挑选key删除。

volatile-lru 从设置了过期时间的结果集中挑选上次使用时间距离现在最久的key开始删除

volatile-ttl 从设置了过期时间的结果集中挑选可存活时间最短的key开始删除(也就是从哪些快要过期的key中先删除)

volatile-lfu 从过期时间的结果集中选择使用频率最低的key开始删除(这是Redis 4.0版本后新增的策略)

12. 常见问题

1. 缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存在不命中时需要去数据库查询,查不到数据则去数据库查询,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决方案

1. 对空值缓存:
如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。缺点:对内存占有消耗过大。

2. 设置可访问的名单(白名单):

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

采用布隆过滤器:布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图) 和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。) 将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

3. 进行实时监控:
当发现redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

用户为什么能在前端发送一个后端不存在的数据??需要进行逻辑处理。

真实UR- http://localhost:8080/user?id=1001 加密后的1001为afdafda79u92dsvdsaf 所以前端显示http://localhost:8080/user?id=afdafda79u92dsvdsaa

后端:afdafda79u92dsvdsaf解密得到100,若前端恶意拼接,解密失败,数据格式不正确

2. 缓存雪崩

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

1. 使用锁或队列:

一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。

2. 设置过期标志更新缓存:

记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。

3. 将缓存失效时间分散开:

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

3. 缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

1 使用锁,单机用synchronized,lock等,分布式用分布式锁。

2 缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存。

3 在value设置一个比过期时间t0小的过期时间值t1,当t1过期的时候,延长t1并做更新缓存操作。

4 数据预热

4. 缓存倾斜

缓存中的某个key突然成为高热点key,比如明星离婚,这样导致大量的用户突然高并发的访问这个高热点key所在的那台缓存服务器,最终导致那台缓存服务器崩掉,继而请求又到达下一个缓存服务器,下一个缓存服务器又承受不住而崩掉,最终导致整个缓存模块崩掉。(是缓存崩掉了,跟数据库关系不大)

数据倾斜的原因及解决方案:

1. 存在bigkey

需要业务层避免bigkey,数据量大的key,将集合类型的bigkey拆分为多个小集合。

2. slot手工分配不均

方案一:

将一些特别热点的key直接放在客户端进行存储,设置过期时间,过期后再从redis中查询。

方案二:负载均衡

我们可以将这个热点key复制出多个子key,每个子key的value值一样,查询的时候使用hash取模算法,将压力分摊到不同的节点。或者存储在二级缓存比如jvm缓存中

方案三:

增加实例配置/集群配置 。

5. 分布式锁

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁

  2. 基于缓存(redis等)

  3. 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点:

  1. 性能:redis最高

  2. 可靠性:zookeeper最高

设置锁

使用redis实现分布式锁最简单的方案是在获取锁之前先查询一下以该锁为key的value存不存在。SET my_key my_value NX PX milliseconds其中,NX表示只有当键key不存在的时候才会设置key的值,PX表示设置键key的过期时间,单位是毫秒。

如果存在,则说明该锁被其他客户端获取了,否则的话就尝试获取锁,获取锁的方法很简单,只要以该锁为key,设置一个随机的值就行了。

比如,我们有一批任务需要由多个分布式线程处理,每个任务都有一个taskId,为了保证每个任务只被执行一次,在工作线程执行任务之前,先获取该任务的锁,锁的key可以为taskId。

释放锁

使用DEL命令将锁数据删除

问题

但是,考虑这样一种情况:客户端A获取锁的时候设置了key的过期时间为2秒,然后客户端A在获取到锁之后,业务逻辑方法doSomething执行了3秒(大于2秒),当执行完业务逻辑方法的时候,客户端A获取的锁已经被redis过期机制自动释放了,因此客户端A在获取锁经过2秒之后,该锁可能已经被其他客户端获取到了。当客户端A执行完doSomething方法之后接下来就是执行releaseLock方法释放锁了,由于前面说了,该锁可能已经被其他客户端获取到了,因此这个时候释放锁就有可能释放的是其他客户端获取到的锁。

会出现释放了别的客户端申请的锁的问题,那么该如何进行改进呢?

有一个很简单的方法是,我们设置key的时候,将value设置为一个有标志性的值,当释放锁,也就是删除key的时候,不是直接删除,而是先判断该key对应的value是否等于先前设置的值,只有当两者相等的时候才删除该key,由于每个客户端产生的随机值是不一样的,这样一来就不会误释放别的客户端申请的锁了。

在redis主从结构下,出于性能的考虑,redis采用的是主从异步复制的策略,这会导致短时间内主库和从库数据短暂的不一致。试想,当某一客户端刚刚加锁完毕,redis主库还没有来得及和从库同步就挂了,之后从库中新选拔出的主库是没有对应锁记录的,这就可能导致多个客户端加锁成功,破坏了锁的互斥性。

6. 扫出指定模式的key列表

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,使用keys指令可以扫出指定模式的key列表。但是,由于redis是单线程的,如果这个redis正在给线上的业务提供服务,keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

scan

这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表, scan命令或者其他的scan如SSCANHSCANZSCAN命令,可以不用阻塞主线程,并支持游标按批次迭代返回数据,所以是比较理想的选择。keys相比scan命令优点是,keys是一次返回,而scan是需要迭代多次返回。 但是会有一定的重复概率,在客户端做一次去重就可以了, scan命令的游标从0开始,也从0结束,每次返回的数据,都会返回下一次游标应该传的值,我们根据这个值,再去进行下一次的访问,如果返回的数据为空,并不代表没有数据了,只有游标返回的值是0的情况下代表结束。 整体所花费的时间会比直接用keys指令长。

你可能感兴趣的:(面试,redis,数据库,缓存)