一、基础
1、泛谈
(1)概念:C 语言编写,高性能,键值对的内存数据库,默认 16个库
(2)优势
- 高性能:读近 110000次/s,写近 81000次/s
- 线程安全:单线程模型
- 多种数据结构:string/hash/list/set/zset/HyperLogLog/Geo
- 持久化:RDB/AOF
(3)高性能原理
- 内存中处理
- IO 多路复用机制
- 单线程少了 线程之间切换的消耗
2、数据结构
(1)类型
- String:embstr/raw/int,字符串/数字/序列化对象
- Hash:ziplist/ht,限元素 2^32-1
- List:ziplist/linkedlist,元素限 2^32-1
- Set:intset/ht,限元素 2^32-1
- Zset/Sorted Set:ziplist/skiplist,限元素 2^32-1
- HyperLogLog:底层是 string
- Geo:底层是 zset
- BitMap:底层是 string
(2)应用
- String:缓存、限流、计数器、分布式锁、分布式Session
- Hash:存储用户信息、用户主页访问量、组合查询
- List:微博关注人时间轴列表、简单队列
- Set:赞、踩、标签、好友关系
- Zset:排行榜,feed流
- HyperLogLog:实时统计(有一定误差)
- Geo:附近的人
- BitMap:布隆过滤器、日活用户/用户签到
(3)实际应用
- 存储对象(string 强调整体查询,hash强调分散更新)
- 集群系统中可用来生成唯一自增主键(string 自增计算 incr)
- 一定时间内限制投票次数(每次投票存到 string并设置过期时间,每次判断存在值则判断次数,不存在则可直接投)
- 不同等级用户限制一段时间内的请求数(计数器 + 过期时间,计数器利用 incr的最大值 9223372036854775807 去减总数,加到了就抛异常)
- 用户购物车存储(hash 存储,以用户编号为 key,商品编号为 field,商品数量为 value,另一个 hash存所有商品信息可按类别再细分)
- 抢购不同类型的充值卡(hash 存储,不同类型为 field,数量为 value)
- 点赞(要求顺序用 list 存储用户点赞用rpush 添加记录,不要求顺序用 set)
- 关注粉丝列表(list 存储,先关注的粉丝放在前面 lpush,取消用 lrem)
- 最近浏览商品(list 存储,ltrim 保证个数,lpush前先 lrem保证唯一)
- 随机推送新闻/微信随机抽奖(set 存储,srandmember 随机取出,或 spop)
- 共同好友(set 的集合操作 sinter)
- 商品列表不同维度组合筛选(set 存储每个维度的商品列表,sinter合并满足所有条件的商品)
- 网站 UV & IP 统计,黑白名单(set 的不重复性)
- 排行榜(zset 存储,zincrby添加数量,zrevrange倒序返回)
- VIP到期任务顺序执行(zset 排序优先级)
- feed 流(个人页是一个 zset流,关注的人也是一个 zset流,每个人发布信息就会更新自己的 个人页zset流 和 粉丝的关注zset流)
- 3、常用命令
https://blog.csdn.net/qq_20475615/article/details/99748166
4、事务
(1)操作
- multi:开始事务
- exec:执行事务
- discard:取消事务
- watch:事务(必须在事务前提下起效)开始前监听 某些key,一旦这些key 的值在 watch后提交事务前被改变,则整个事务不执行,不会存在部分执行(包括事务中其他涉及没监听的 key也不会执行,因为它把命令队列清空了)
(2)原则
- 开始事务 到 执行/取消事务之间,不会被其他客户端打断,分 ①开始事务 ②命令入队 ③执行提交事务
- 语法错误/编译期错误时,事务整个失败(因为没执行就可发现错误);运行错误/运行期错误时 部分语句执行失败 其他不会回滚
- watch 基于 CAS思想,只有在事务中那段语句才有校验,执行前会校验值是否被改过(exec 失败后因为已经知道最新值,若再去执行相同事务则可以成功,除非再次 watch且其他人又改动了)
5、Lua 脚本
(1)优点
- 减少网络开销:多个命令放一个脚本,一次性执行
- 原子操作:整个脚本一起执行,不会被其他客户端插入
- 复用:脚本会存 redis,其他客户端可复用
(2)使用
# 1、执行 eval:EVAL script numkeys key [key ...] arg [arg ...]
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 k1 k2 v1 v2
# 2、eval 调用 redis 命令:redis.call遇错误会返回错误信息并中断执行,redis.pcall 则不返回错误并继续执行
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 k1 v1
eval "return redis.pcall('set',KEYS[1],ARGV[1])" 1 k1 v1
# 3、把脚本装载到 redis,然后用 evalsha执行,可复用
script load "return redis.call('set',KEYS[1],ARGV[1])" // 这里返回一个 sha
evalsha <sha> 1 k1 v1
# 4、执行一个 lua脚本文件
redis-cli --eval test.lua
6、管道
命令一次性发到服务器,但没有原子性,也就是不保证能一起执行,并且执行的顺序也都没法确定,它只是节省了多次网络通信
7、key设计
- 表名:主键名:主键值:属性名(如 sms:id:12:name)
- 对key进行分类
- key不要太大,最好不超过1k
二、内存回收
1、过期策略(redis默认用 定时+惰性)
- 定时删除:给每个key设置定时器,到时间就删,特别耗cpu
- 定期删除:每隔100ms随机抽取 20个设置了过期时间的key,检查如果过期就删除,随机抽取的原因是没办法每次都处理所有的key,否则给cpu很大压力,所以随机抽取到的过期key就会删除,抽不到的则不会删
- 惰性删除:当那些定期删除时没有被删掉的数据,等到有人使用时发现过期了,这时再去删除,这就是惰性
2、淘汰机制:定期和惰性都没删除的key积累过多导致内存堆积和很多过期数据直到内存不足则使用淘汰机制,这个需要配置 redis.conf,有多种淘汰机制
#配置淘汰机制,默认是 noeviction
maxmemory-policy volatile-lru
#配置开始淘汰的数据阈值,默认 0 表示不限制
maxmemory 100mb
- volatile-lru:有过期时间 && 最近最少用即最后一次访问时间最久的
- volatile-ttl:有过期时间 && 淘汰时间最近
- volatile-random:有过期时间 && 随机
- volatile-lfu:有过期时间 && 最少使用也就是总使用次数最少,4.0版本后增加的
- allkeys-lru:所有key && 最近最少用
- allkeys-random:所有key && 随机
- noeviction:直接不让写入新的数据
- allkeys-lfu:所有key && 最少使用,4.0版本后增加的
三、持久化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble
开启)
1、RDB 快照(Redis DataBase):给内存里某个时间点的数据创建一个快照,相当于一个数据副本,创建后可以把该快照复制到本地或者其他服务器便可使用。
(1)配置
# 可同时配置多个 触发条件
save 900 1 //表示每15分钟且至少有1个key改变,就触发一次持久化
save 300 10 //表示每5分钟且至少有10个key改变,就触发一次持久化
save 60 10000 //表示每60秒至少有10000个key改变,就触发一次持久化
save "":关闭 // 单配置这个表示关闭 RDB
dbfilename dump.rdb // rdb文件名
(2)触发情况
- 定时执行配置文件的save策略
- 主从同步全量复制触发
- 手动执行 save/bgsave命令,前者阻塞,后者非阻塞
- 手动执行 flushall命令
- 手动执行 shutdown命令
(3)工作原理
- 父进程先判断是否有在 生成 rdb文件或者 aof重写,若没有则往后做
- 父进程 fork一个子进程,此时是阻塞的
- 父进程处理客户端请求,子进程将数据集写入到一个临时 RDB 文件中,此时是非阻塞的,cow/copy on write方式
- 完成后再覆盖原来的 rdb文件,该文件压缩过所以比较小便于传输(这样出现异常不会覆盖,保证安全)
(4)特点:恢复时快、体积小,但较容易丢失数据,所以适合每日备份
(5)恢复方式:将 .rdb文件放到配置的路径dir,默认安装目录下(CONFIG GET dir 命令查看目录),重启时自动执行
2、AOF 追加文件(Append Only File):把执行的命令追加写入硬盘中的AOF文件。
(1)配置
# 打开 AOP方式
appendonly yes
#有下面三种 持久化策略,只能配一种
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
appendfilename "appendonly.aof" // 文件名
(2)重写策略 BGREWRITEAOF:重写为了省空间
no-appendfsync-on-rewrite // 配置该属性打开重写
auto-aof-rewrite-percentage 100 // 比上次重写大100%,也就是大一倍
auto-aof-rewrite-min-size 64mb // 大于64m
第一次重写都是以大小为判断标准,比如文件已经超过了64m了,如果还没重写过那此时就执行 bgrewriteaop,如果重写过那就判断有没有比上次重写时大一倍
(3)重写原理
- 父进程fork一个子进程,此时是阻塞的,若有 aof重写进程则返回,若有 rdb快照在执行则等它完成才能重写
- 子进程开始将新 AOF 文件的内容(即重写后的)写入到临时文件
- 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾(就是缓冲区和原AOF文件都写一份),即使在重写时停机,现有的 AOF 文件也还是安全的
- 子进程完成重写,它给父进程发送信号,父进程在接收到信号之后,将内存缓存中的数据追加到新 AOF 文件的末尾。替换旧文件
(4)特点:恢复时慢、体积大,但不容易/较少丢失数据且追加时比较快
(5)恢复方式:将 .rdb文件放到配置的路径dir,默认安装目录下(CONFIG GET dir 命令查看目录),重启时自动执行,若打开,默认按AOF来恢复而 .rdb不会执行
(6)文件修复:使用 redis-check-aof –fix 它会丢弃错误的指令
3、抉择
- 如果只是当缓存,不开也没太大关系
- 如果能接受几十秒以上的丢失,可RDB
- 只能是秒级别内的,就得AOF(此时可以搭配较长时间时 RDB一下)
- 多数配主从,slaver来备份、读写分离、宕机升级
四、主从复制/读写分离
1、配置 redis.conf
slaveof <masterip> <masterport> //从节点配置文件中指定主库地址和端口
slave-read-only yes //从节点配置从库只读
masterauth <password> // 若主节点有密码,配置
#bind 127.0.0.1 // 主节点注释本地绑定
protected-mode no // 主节点关闭保护模式
//启动时指定配置,在 src目录下
./redis-service ../redis.conf
//可以连进主节点和从节点的 redis命令窗口,用 info看节点状态
info
2、同步复制:复制过程是异步的
(1)全量复制:slave 第一次连 master
- slave 发送 psync命令到 master
- master执行 bgsave存到rdb文件,然后发给slave,slave保存文件到本地然后加载到内存
- master同步过程中新的命令会写到命令缓冲区,最后会发给slave同步
(2)增量复制 (2.8版本后可选)
- 若slave没同步过,则发送(psync ? -1),若复制过则发(psync <runid> <offset>)到 master
- 若master回复(+fullresync <runid> <offset>)则全量同步,若(+continue)则增量,增量则不需要生成rdb文件,而只是将命令缓冲区的数据发给slave
(3)正常连接:主从没有断开的情况下,master 会发送一连串的命令流来保持对 slave 的更新(就是主执行了一条命令,就发给 从机也去执行)
无硬盘复制 repl-diskless-sync:全量复制是生成了 rdb文件到磁盘,再加载到内存发给slave,但无硬盘复制是子进程直接发 rdb文件过去,不生成到硬盘。
3、master 宕机
- 手动在slave上执行 slaveof no one 命令,提升为新master
- 假如原先该slave只是配置了可读,则将其配置为可写,config set slave-read-only no,config rewrite(rewrite才会更改文件,否则重启就恢复了)
- 配置其它slave从新master同步,slaveof <ip> <port>
- 告诉客户端新的地址(假如客户端和redis之间没有用 keepalived进行虚拟ip转移的话)
4、过期 key处理
- slave不会让key过期,而是在master中key过期后,master生成一条del命令,让slave进行删除key。
- 在key过期后到master发出的del命令到达slave的时间空隙,slave会用自己的逻辑时钟来判断key是否过期而决定能否被读取。
- lua 脚本执行器期间不会执行过期key操作。
- 补偿:我们也可以在代码中增加 ttl 的判断,来多一层判断过期key。
5、建议
- 主从最好在同个局域网,保证速度和连接稳定
- 配置master不持久化到硬盘造成开销,而让slave持久化。但需注意假如重启master会从空数据集开始,如果此时有slave同步,slave数据就被清空了,所以如果用这种模式千万不要让master故障时自动重启。
- 如果多个 slave宕机后同时重启,大量同步命令发给 master去同步可能搞垮 master(所以要注意分开启动)
五、哨兵机制
1、简介
- 哨兵集群可以监控主从且哨兵之间也会相互通信,实现master故障转移,重新选举master
- 主观下线是某个sentinel说某服务器下线了,客观下线就是sentinel集群集体协商后说某服务器下线。客观下线只是对于master而言,而其它类型比如slave则只要一个sentinel说它下线不需要经过协商
2、配置 sentinel.conf
// 安装目录下有默认的哨兵配置文件 sentinel.conf
//配置指示 Sentinel 去监视一个名为 mymaster 的主服务器, 这个主服务器的IP 地址,端口号,而将这个主服务器判断为失效至少需要 2 个 Sentinel 同意
sentinel monitor mymaster <ip> <port> 2
port XXX -- 端口
//弄好三份这个配置 sentinel1.conf、sentinel2.conf、sentinel3.conf(记得三份的port设置不同),然后到 src分别以他们启动,记得命令跟 cli的不一样
./redis-sentinel sentinelX.conf
3、工作原理
- 每个sentinel每1s的频率ping每台master、slave和其它sentinel,最后一次ping成功距离现在超过 down-after-milliseconds,则判断为该机主观下线 SDown
每个sentinel还会以10s一次的频率发送 INFO命令给每个master和slave,但如果master 被主观下线,则INFO命令10s改为1s
每个sentinel以2s一次在sentinel集群间交换自己得到的主从节点信息 - 如果是master主观下线,则其它sentinel也ping该机,如果达到配置的sentinel同意数量,则master被认为客观下线 ODown
- 如果判断 master下线的同意数不够,则解除;否则选出一个sentinel leader(利用 Raft算法),并进行新master 的选举
- 哨兵leader发SLAVEOF NO ONE 命令告诉选中的节点当选,修改其相应的配置,然后再告诉其它从节点连接新master,而旧master如果恢复了也成为新master的从节点
六、集群
1、简介
- ping-pong机制:所有 主节点彼此互联,一旦超过半数主节点认为某节点 ping不同,则认为它 下线。
- 哈希槽:一种取模和一致性哈希折中的办法,直接取模会导致数据和节点紧密关联,缺乏灵活扩展;而一致性hash在扩容或缩容情况下,部分数据需要重新计算节点。根据key 的CRC16算法,得到的结果再对16384进行取模。槽个数是固定的,需要根据实际情况预先定下槽的数量,redis cluster槽数量是16384个。
- 集群崩溃:某节点主和从都挂了,槽缺少了一部分则认为集群不存在。另一个情况就是超过半数的主挂了也认为集群不存在。
2、配置
//集群至少要 3master
//复制几个配置文件,然后下面对应的涉及端口的都改掉port 6379
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file "nodes-6379.conf"
logfile "cluster-6379.log"
dbfilename dump-cluster-6379.rdb
appendfilename "appendonly-cluster-6379.aof"
//然后注意跟主从一样要 主节点注释本地绑定,同时关闭保护模式
#bind 127.0.0.1
protected-mode no
//然后分别指定 src下然后启动
./src/redis-server cluster-XXX.conf
//redis-5.0.0版本开始才支持“--cluster”;-a 后面是连接密码,如果没有就不用;replicas后的数字就是每个 master 跟着几个slaver,0就是不用slave
./src/redis-cli -a Object --cluster create --cluster-replicas 0 8.129.219.177:6379 8.129.219.177:6380 8.129.219.177:6381
ps -ef|grep redis
cluster nodes
cluster info
//redis-5.0.0版本之前得先装 Ruby这里不详说
//然后客户端连接测试,带上 -c,可看到存取值会带上 hash过程
./src/redis-cli -c -p 6380
3、扩容/缩容:https://blog.csdn.net/iteen/article/details/102718048
- 增加节点:需要从每个已有节点拿去部分槽过来
- 删除节点:把数据迁移到其它节点才能删除
- 节点崩溃:集群不可用
- 集群主从:集群中每个节点的主从模式,集群中如果每个点只有一台机,宕机了那么那部分槽的数据就丢失了,导致集群不可用,所以给每个节点配置一定量的从节点,这样宕机了它的从节点就顶上,那部分分片数据就又有了,集群继续可用。只是cluster提供了故障转移而不用加sentinel
4、缺陷
- key批量操作限制,比如 mget这些,因为数据在不同节点上
- 事务限制,数据多节点而事务只是在一个节点上起效果
- 多数据空间不支持,每个节点只有db0
- 分片是对于key而言的,所以value是不能拆分的
5、其它分片机制
- redis-sharding
- codis
- twemproxy
七、应用
1、分布式锁
(1)要点:
- 互斥:同一时刻只能一个人拥有
- 避免死锁:给锁加过期时间
- 加锁者自己释放锁:锁带上客户端标识,可用于释放时判断加锁者(如 value存 requestId)
- 业务没完锁超时:开分线程,为锁续命
(2)redlock 算法
- 先获取开始时间,然后使用 key 和唯一性的 value 尝试在redis集群中 N 个点上加锁
(这里的节点要注意:要么是 N个redis单实例,要么是 N个集群,而不是只有一个有N个节点的集群。假如只有一个集群,那么因为redis集群的哈希槽,key相同都是落在相同的节点,也就不存在去多个节点加锁的可能性) - 当且仅当在大多数(N/2 +1)上获取到锁,结束时间减去开始时间就是获取锁的时间,假如这个时间小于预先定好的获取锁超时时间,则认为获取到锁
- 若获取到锁,则key有效时间为 锁过期时间 - 获取锁的时间;若没获取到锁,则需要在每个加过锁的地方上都分别解锁
(3)框架 redisson
- 封装了 redlock算法,在集群中 hash节点后选一台机器执行 lua脚本
- 增加了watchdog 延期锁过期时间功能。每隔10s 就把超时时间又改为30s,假如断开了也就不会续租了
- 可重入锁,锁是用的 hset存储,key:{"XXX":1},前面是客户端id,后面是加锁次数,加一次锁就增加1,最后解锁相应的次数即可,数字到0则失去锁
- 其他加锁:其他客户端加锁,发现有锁它会拿到目前锁的剩余时间
- 删除锁:因为可重入,所以记录了加锁次数,当解锁解到次数为 0 了,则表示不持有锁了
(4)优化
- 场景:分布式互斥锁假如获取到之后,处理业务时间过长会导致并发量很低,假如用在下单时如果把整个下单流程放在获取分布式锁和释放之间,那么一秒钟也处理不了多少请求
- 解决:需要在锁中间执行的尽量粒度不要太大;其次可以模仿 ConcurrentHashMap的分段加锁思想,比如库存中1000个分成10段,每个100去加锁获取
2、数据缓存
3、发布订阅/队列
- 订阅:订阅一个频道用于监听该频道发布的消息,SUBSCRIBE redisTest
- 发布:发布消息到一个频道,PUBLISH redisTest "send test"
4、排行榜:例如 网站的文章,可以用 zset存储,key是文章id,score是时间/点赞数,这样就可以利用redis完成文章按时间排序显示、文章按点赞数排序
5、session 共享/单点登录
6、统计网站
7、频率控制:例如 文章评论/点赞的次数,假如文章限制一个人只能评论/点赞一次,我们可以放到redis的set里面,可以进行校验
8、防重提交:可以存 set,校验成员重复性
八、扩展
1、缓存 穿透、雪崩、击穿、热key
(1)穿透:大量请求请求到缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉
- 可采用布隆过滤器,将可能存在的数据哈希到一个大的bitmap中,根本不存在系统中的数据就会被 bitmap拦截掉,然后避免对底层存储系统的压力
- 缓存一个空值,设置较短过期时间
(2)击穿:某个热key过期的瞬间来大量请求使用该key,导致redis没命中,数据库奔溃
- 失效时查库前加互斥锁,保证只有一个线程去查库
(3)雪崩:缓存同一时间大面积失效,请求都落到数据库造成数据库短时间接收大量请求奔溃
- key过期时间加随机数,不让其在同一时间过期
- 加本地 encache二级缓存
- 限流/降级(两窗两桶)
(4)热key
- 项目启动时缓存
- 提供接口缓存
2、并发竞争 key:Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同
- 不要求顺序:分布式锁
- 要求顺序(二选一):①分布式锁 + 时间戳 ② 消息队列
3、数据一致性
(1)先更新数据库,再删缓存
- 问题场景:可能删缓存失败,导致后面请求拿到旧数据,但一般失败几率较低
- 解决方案:可用消息队列去重试删除;或直接删除发到mq,mq消费去删除缓存成功才ack确认,保证最终一致(可考虑结合数据库 binlog日志)
(2)先删缓存,再更新数据库
- 问题场景:可能在删完去更新数据库时,已经有请求到达并把旧数据缓存进去/也可能后面更新数据库的时候顺序跟更新缓存顺序不一致
- 解决方案:对于同个key的数据,加锁等到更新完成才给查询/删缓存而不是更新缓存
(3)删缓存而不是更新缓存
- 缓存的数据可能需要经过计算,可能更新10次请求才用到一次,这样造成了浪费
- 更新缓存可能有线程问题,比如先更新数据库的线程后更新缓存,结果造成数据错乱
(4)数据库主从同步延迟
- 问题场景:关系数据库可能在操作完主库后,从库还没同步到,拿到旧数据
- 解决方案:mysql5.5开始的半同步复制,不马上返回结果而等到至少一个从库同步才返回;配置binlog不同步硬盘,sync_binlog=1,innodb_flush_log_at_trx_commit=1;使用mysql5.7并行复制
4、布隆过滤器
(1)原理:判断数据是否存在,若说存在则不一定在,若说不在则一定不在。存储数据小,如用户id要64字节,bloomfilter可能只要1字节。底层用了 bit数组,利用多个哈希函数去计算某个值,决定数组哪几个位的标志设置为1。然后在判断的时候,还是按照插入时候一样先计算查询的值对应的数组中的几个标志位,然后去那几个位查看状态是否都为1,只要有一个不是1,说明它肯定不在范围内;但假如都是1,则说明它可能存在,因为可能出现多个值都用到某个相同的位,所以说布隆只能判断到某值它可能存在就是这个原因。
(2)实现
- guava bloomfilter
- redisson RBloomFilter
- JReBloom+bloomfilter插件(redis4.0)
5、IO多路复用
(1)简介:redis 内部使用文件事件处理器 file event handler
,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
(2)文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
(3)多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
(4)客户端与 redis 的一次通信过程:
- 客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个AE_READABLE事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的AE_READABLE事件与命令请求处理器关联。
- 假设此时客户端发送了一个set key value请求,此时 redis 中的 socket01 会产生AE_READABLE事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的AE_READABLE事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的key value并在自己内存中完成key value的设置。操作完成后,它会将 socket01 的AE_WRITABLE事件与命令回复处理器关联。
- 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个AE_WRITABLE事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如ok,之后解除 socket01 的AE_WRITABLE事件与命令回复处理器的关联。
6、LRU:Java可用LinkedHashMap实现,redis用 哈希链表 实现
7、对比 memcached
- redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
- 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的。
- Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
8、处理故障
- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
- 事后:利用 redis 持久化机制保存的数据尽快恢复缓存
9、常见规范:https://developer.aliyun.com/article/531067
10、禁用危险命令
// 主配置文件中增加
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
九、性能优化
1、慢查询日志
(1)简介:redis慢日志是指一个系统进行日志查询超过了指定的时长。这个时长不包括IO操作,比如与客户端的交互、发送响应内容等,而仅包括实际执行查询命令的时间
(2)配置
slowlog-log-slower-than 10000 :超过的时长,微秒
slowlog-max-len 128:最大长度
2、优化策略
- 利用管道减少连接
- 合适的持久化策略
- 引入读写分离
- 防止大 key
十、springboot 集成
Spring Boot 1.0 默认使用的是 Jedis 客户端,2.0 替换成 Lettuce
1、使用 Jedis
- ① 引入依赖
<!-- Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<!--<artifactId>spring-boot-starter-data-redis</artifactId>--><!--这个是springboot1.5版本及之后的引入方式-->
<version>1.4.7.RELEASE</version>
</dependency>
- ② 配置文件application.yml增加配置
spring:
redis:
database: 0
host: 192.168.1.60
port: 6379
# password:
timeout: 15000 # 毫秒
jedis:
pool:
max-active: 8 # 最大连接
max-wait: -1 # 负值表示无限制
max-idle: 5 # 最大空闲连接
min-idle: 0 # 最小空闲连接
- ③ 增加配置类
@Configuration
public class RedisConfig {
@Autowired
JedisConnectionFactory jedisConnectionFactory;
@Bean
public JedisPool jedisPool() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(500);
return new JedisPool(jedisPoolConfig,
jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort(),
jedisConnectionFactory.getTimeout(), jedisConnectionFactory.getPassword());
}
}
- ④ 尝试操作一下
@Component
public class RedisUtil {
@Autowired
JedisPool jedisPool;
public void getValue(){
Jedis jedis = jedisPool.getResource();
jedis.set("name","lincheng");
System.out.println(jedis.get("name"));
}
}
- ⑤ 测试类走一波
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProjectApplicationTests {
@Autowired
RedisUtil redisUtil;
@Test
public void test(){
redisUtil.getValue();
}
}
2、如果使用 RedisTemplate ,上面的 ①②⑤可以保持不变,③不需要,④ 修改如下,之所以可以直接注入RedisTemplate,因为 RedisAutoConfiguration已经实例化bean,我们可以查看该类中的源码得知
-
@Component public class RedisUtil { @Autowired RedisTemplate redisTemplate; public void getValue(){ redisTemplate.opsForValue().set("name","cheng"); System.out.println(redisTemplate.opsForValue().get("name")); } }
3、对象存储,以上都是不涉及对象类型的操作,如果涉及对象存储,按照上面的 jedis配置操作会报序列化异常,有两个解决方式,第一种让每个操作的实体类实现 Serializable 接口再进行操作即可,这里重点介绍第二种。注:RedisTemplate会使用jdk自带的序列化方式所以不会异常。
- pom文件增加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
- 增加一个序列化实现类
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
private Class<T> clazz;
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (null == t) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (null == bytes || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return (T) JSON.parseObject(str, clazz);
}
}
- RedisConfig类如果是jedis的保留原有的配置,再增加以下,如果是RedisTemplate方式,直接增加以下
@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用fastjson序列化
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
- 最后测试即可
@Data
public class Child{
private String name;
private Integer age;
}
@Component
public class RedisUtil {
@Autowired
RedisTemplate redisTemplate;
public void getValue(){
Child child = new Child();
child.setAge(1);
child.setName("明");
List<Child> list = new ArrayList<>();
list.add(child);
redisTemplate.opsForList().rightPushAll("lName",list);
System.out.println(redisTemplate.opsForList().range("lName",0,10));
}
}
十一、线上问题
如果 持久化文件比如 RDB太大,服务器内存不足则可能导致 多次持久化失败,redis挂
解决是:
vi /etc/sysctl.conf
然后添加:vm.overcommit_memory=1 然后退出按Esc+:wq
重新启动或运行命令“sysctl vm.overcommit_memory=1”以使其生效。