02 Redis.txt
UP 返回
视频4-1
1 主流应用架构
客户端 缓存器 存储层
客户端向缓存器请求数据,如果存在则直接返回;不存在则穿透查询存储层,查询的结果再回写到缓存层(回种),并返回客户端;同时还有熔断机制(当存储层不能提供服务时,客户端的请求直接由缓存层处理,不论有没有结果都直接返回,从而能在有损的情况下对外提供服务)
2 缓存中间件
Memcache 代码层次类似于hash
支持简单数据类型(string);不支持数据持久化存储;不支持主从同步;不支持分片(打碎数据库,将大数据分布到多个物理节点上的方案)
Redis
数据类型丰富(set list等);支持数据磁盘持久化存储;支持主从同步;支持分片(redis3.0开始)
有数据持久化的需求或者对数据结构处理有高级要求的使用redis,其他简单的key-value存储直接用memcache即可
3 redis为什么快
redis拥有100000+QPS (query per second,每秒内查询次数)
完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高,不会受到硬盘IO的影响(单继承单线程的c语言编写)
数据结构简单,对数据操作也简单(存储结构就是键值对,类似于HashMap,查找操作复杂度基本是O(1)。性能比关系型数据库要高很多)
采用单线程,单线程也能处理高并发请求,想多核也可启动多实例
主线程是单线程(包括IO事件的处理以及IO对应的相关业务的处理,过期键的处理,父子协调,集群协调等,这些逻辑会被封装成周期性的任务由主线程周期性的处理)。这样对客户端所有读写请求就是串行的处理,多个客户端的写操作就不会有并发问题,避免了频繁的上下文切换和锁竞争,效率更高。
单线程是指处理网络请求的时候单,但是实际运行的时候肯定不止一个线程。比如进行数据持久化时,会根据实际情况以子进程或子线程的方式执行
使用多路I/O复用模型,非阻塞IO
redis需要在多个平台运行,所以会根据编译平台的不同选取不同的IO多路复用函数作为子模块,提供给上层统一调用的接口
会优先使用时间复杂度为O(1)的I/O多路复用函数作为底层实现(时钟的evport,linux中的epoll,macOS中的kqueue,这些性能都比select优秀)
因为select在所有系统上都会实现,所以以时间复杂度为O(n)的select作为保底方案。一旦当前系统没有上述比select更优秀的函数就选择select作为备选方案
基于react设计模式监听I/O事件
视频4-2
dbsize 查看redis数据量
4 redis数据类型 底层的数据类型有:简单动态字符串;链表;字典;跳跃表;整数集合;压缩列表;对象
String 最基本的数据类型,即键值对,值最大能存储512m;二进制安全,所以可以包含任何数据,比如jpg图片或者序列化的对象
set name "redis" 设置一个值为redis的name键
get name 输出redis
set count 1 设置count值为1
get count
incr count count自增(redis的单个操作都是原子性的,即一个事务是一个不可分割的最小工作单位,事务中包括的操作要么都做要么都不做,所以不用考虑并发问题)
get count
比如需要统计某个页面用户每天的访问次数,只需要incr userId201231(userId+当日的时间戳),这样就可以统计了
保存字符串对象的结构:
struct sdshdr{
int len;//buf中已占用空间的长度
int free;//buf中剩余可用空间的长度
char buf[];//数据存储空间
};
Hash String元素组成的字典,适用于存储对象
hmset lilei name "Lilei" age 26 title "Senior" 设置一个叫lilei的hash,拥有属性name age title
hget lilei name 打印name属性
hset lilei title "Pricipal" 修改title属性
hget lilei title
List 列表,按照String元素插入顺序排序。大约能存储40亿个成员
lpush mylist aaa 向列表mylist添加元素aaa
lpush mylist bbb
lpush mylist ccc
lrange mylist 0 10 从mylist第0位取出10个元素(这个mylist只会打印三个元素,同时元素是后进先出,即ccc bbb aaa)
可以实现最新消息排行板的功能,因为越新插入的消息越先显示
Set string元素组成的无序集合,通过哈希表实现,不允许重复。删除查找O(1)
sadd myset 111 向myset插入111
sadd myset 222
sadd myset 333
sadd myset 222 此次插入失败,元素重复
sadd myset abc
sadd myset abd
smembers myset 查看set所有元素。是无序的
在微博应用中可以将一个用户的所有关注人存在一个集合中,所有粉丝存在一个集合中。redis为集合提供了方便的交并差等操作,就能很好实现共同关注共同喜好等功能
Sorted Set 通过分数来为集合中的成员进行从小到大的排序。不允许重复元素,分数可以重复
zadd myzset 3 abc 插入元素abc,分数为3
zadd myzset 1 abd
zadd myzset 2 abb
zadd myzset 1 abd 此处插入失败
zadd myzset 1 bgg
zrangebyscore myzset 0 10 查看元素(会以分数从低到高排序)
可以用来存储全班同学的成绩,score为得分,value为学号,这样添加完毕以后就已经排好序了。还可以用这个来做带权重的队列,比如给不同任务分配不同的分数来优先执行
用于计数的HyperLogLog,用来支持存储地理位置信息的Geo
视频4-3
5 从海量key里查询某一固定前缀的key
首先要摸清数据规模,即问清楚边界
如果使用keys pattern指令会一次性返回所有匹配的key ,键的数量过大会使服务卡顿,对内存的消耗和redis服务器都是隐患。
所以使用SCAN cursor [MATCH pattern][COUNT count],可以无阻塞的提取出符合条件的key列表,每次执行只会返回少量key,不会像keys可能阻塞服务器
基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历
不保证每次执行都返回某个给定数量的元素,支持模糊查询。甚至可能返回0个元素,但是只要返回的游标不为0,当前遍历就不应该结束。数量多的时候可能一次返回十几个,数量少的时候可能一次全部返回,等同于keys
一次返回的数量不可控,只能是大概率符合count参数
scan 0 match a* count 4 返回符合a*的key集合。该语句会返回两个值,第一个表示本次遍历以后的游标,用于下次遍历,后一个返回值为返回的key
例如返回:1) “2”
2)1)“a9”
那么下一次遍历应该是scan 2 match a* count 4。同时每一次返回的游标不一定会比上一次的小,所以可能会返回重复的key,需要在外部程序自己去重
视频4-4
6 通过redis实现分布式锁
分布式锁需要解决的问题:互斥性(任意时刻只能由一个客户端获取锁);安全性(锁只能被持有该锁的客户端删除);死锁(获取锁的程序因为某些原因宕机无法释放锁,其他客户端可能再也无法获取锁,需要处理这种情况);容错(当部分redis结点宕机时,客户端仍然能获取锁和释放锁)
SETNX key value 如果key不存在,则创建并赋值。复杂度O(1),设置成功返回1,失败返回0
setnx locknx task 设置key,返回1
setnx locknx test 设置失败,返回0
这样在执行某一个任务前,可以先使用setnx命令,判断当前是否有锁。但是这样设置的key是长期有效的,所以需要设置过期时间
EXPIRE key seconds 设置key的过期时间的秒数,当key过期时会被自动删除
expire locknx 2
setnx locknx test 2s以后设置成功,返回1。业务中可能出现的伪代码:
RedisService redisService = SpringUtils.getBean(RedisService.class);
long status = redisService.setnx(key,"1");
if(status == 1){
redisService.expire(key,expire);
doOcuppiedWork();//进行独占资源的逻辑
}
以上代码如果在status获取成功以后出错了,key仍然将一直被占用。出现的原因就是原子性得不到满足,虽然setnx和expire本身是原子的,但是在这里被组合以后就不是了
SET key value [EX seconds] [PX milliseconds] [NX|XX] EX表示过期时间的秒,PX表示过期时间的毫秒,NX表示只在键不存在时才对键进行设置操作,XX只在键存在时才进行操作。操作成功返回OK,失败返回nil。从redis2.6.12版本开始允许将两个操作变为一个操作执行
set locktarget 12345 ex 5 nx 设置键locktarget不存在时则将其设为12345(一般可以将其值设置成对应的requestid或线程id,即可以标识当前占用该资源的请求或线程),过期时间为5s。返回OK
set locktarget 12345 ex 5 nx 继续设置5s内会失败返回nil,5s后设置成功。业务中的伪代码:
RedisService redisSerivce = SpringUtils.getBean(RedisService.class);
String result=redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
if("OK".equals(result)){
doOcuppiedWork();//进行独占资源的逻辑
}
以上代码就可以变成原子操作了
如果大量的key同时过期,由于清除大量的key很耗时,会出现短暂的卡顿现象。可以在设置过期时间的时候给每个key加上随机值,可以很大程度避免
视频4-5
7 通过redis做异步队列
使用List作为队列,RPUSH生产消息,LPOP消费消息
rpush testlist aaa
rpush testlist bbb
rpush testlist ccc
lpop testlist 重复执行可以像队列一样取出数据。但是缺点是没有等待队列里有值就直接消费,可以通过在引用层引入sleep机制去调用lpop重试
BLPOP key [key ...] timeout 阻塞直到队列有消息或者timeout后超时(单位s)
blpop testlist 30 此时若testlist没有元素则会超时,30s后会结束;如果中途有元素了则直接取出。但是缺点是只能供一个消费者消费
如果想让多个消费者来消费,可以使用pub/sub主题订阅者模式(发送者pub发送消息,订阅者sub接收消息;订阅者可以订阅任意数量的频道)
subscribe myTopic 在客户端1订阅主题myTopic(该主题无需事先存在)
subscribe myTopic 在客户端2订阅主题myTopic
subscribe anotherTopic 在客户端3订阅主题anotherTopic
publish myTopic "Hello" 在客户端4向myTopic频道发布消息,客户端1 2将收到
publish myTopic "I love you"
publish anotherTopic "hi" 在客户端4向anotherTopic频道发布消息,客户端3将收到
缺点:消息的发布是无状态的,无法保证可达(对发布者来说消息是即发即失的;生产者在消息发布时下线,重新上线是无法接收到的)。要解决这个问题需要使用专业的消息队列,如卡夫卡等
视频4-6
8 redis持久化
RDB(快照)持久化:保存某个时间点的全量数据快照
打开reids的配置文件,其中save的策略信息:
save 900 1 表示900s之内如果有1条写入指令,则进行一次快照,即备份。下面同理(redis每个时段的读写请求是不同的,所以可以根据需要配置多条规则)
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes 设置yes表示当备份进程出错时,主进程停止接收新的写入操作(为了保护持久化数据一致性的问题。如果自己的业务有完善的机制处理这种问题可以关闭这个设置)
rdbcompression yes 表示备份时需要将rdb文件压缩后再做保存(建议设置为no,因为redis本身是cpu密集型服务器,开启压缩会占用更多的cpu效率,相比硬盘成本cpu的速度更重要)
save "" 禁用上述的rdb配置只要加上 save "" 即可
src目录下有一个二进制文件dump.rdb,redis会定期将数据备份到这里。rdb文件可以通过以下命令生成:
SVAE 该命令会阻塞redis的服务器进程,直到rdb文件被创建完毕,很少被使用
BGSAVE fork出一个子进程来创建rdb文件,不阻塞服务器进程(父进程处理自身请求的同时,会通过轮询来接收子进程的信号。子进程文件生成以后会立刻返回OK给父进程)
bgsave命令执行时会先检查有没有aof/pdb的子进程在执行,如果已经有了备份任务,将会拒绝客户端的save bgsave命令(防止子进程竞争);如果没有就调用源码的rdbSaveBackground来调用系统的fork指令创建进程,实现了copy-on-write
lastsave 可以通过此命令查询上次备份时间(返回的是一个integer,如果值不同说明有了新的备份。可以通过这个时间戳生成不同的时间的全量备份文件)
自动触发rdb持久化的方法:
根据redis.conf配置里的SAVE m n定时触发(使用的是BGSAVE命令)
主从复制时,主节点自动触发
执行Debug Reload
执行Shutdown且没有开启AOF持久化
缺点:内存数据的全量同步,数据量大时会由于I/O而严重影响性能;可能会因为redis挂掉而丢失从当前至最近一次快照期间的数据
视频4-7
AOF(Append-Only-File)持久化:保存写状态
记录下除了查询以外的所有变更数据库状态的指令
以append的形式追加保存到AOF文件中(增量)
aof持久化默认是关闭的,可以修改redis配置文件中的appendonly no为yes即可开启。文件名的配置为appendfilename "appendonly.aof"。文件写入方式的配置项如下:
# appendfsync always 一旦缓存区的内容发生变化,就及时的将缓存区的内容写入aof
appendfsync everysec 每隔1s将缓存区的内容写入文件(默认且推荐的方式)
# appendfsync no 将写入aof的操作交给操作系统决定。一般为了效率,操作系统会等到缓存区满了才会将数据一次性写入文件
可以通过config命令修改配置参数:config set appendonly yes
AOF会实时将命令记录下来,文件就会不断增大,redis解决了这个问题,详见视频
redis数据的恢复
redis启动会优先加载aof文件,没有才会加载rdb文件
RDB和AOF对比:
RDB全量数据快照,文件小,恢复快;但是无法保存最近一次快照之后的数据
AOF可读性高,适合保存增量数据,数据不易丢失;但是文件体积大,恢复时间长
RDB-AOF混合持久化方法:redis4.0后推出,并作为默认的方式
BGSAVE做镜像全量持久化,AOF做增量持久化。redis实例重启时,会先bgsave持久化文件重新构建内容,再使用aof文件重放近期的操作指令,就能完整恢复重启之前的状态
视频4-8
9 Pineline
Pineline和linux的管道类似
redis基于请求/响应模型,单个请求处理需要一一应答
pineline批量执行指令,节省多次IO往返时间
有顺序依赖的指令建议分批发送
10 redis的同步机制
DOWN 返回