Redis+Caffeine介绍
2022-04-09 17:11:01 0 举报
AI智能生成
缓存Redis+Caffeine介绍,你真的学会了吗?
作者其他创作
大纲/内容
Redis简介、入门、基础知识、概述
Redis诞生背景
架构演变:文件缓存->Memcache->Mysql主从读写分离->分表分库
新要求
3V
Volume:海量数据
Variety:数据类型、终端设备的多样
Velocity:实时
3高
高可扩:不断扩展现有功能与新功能
高性能
高并发
开发的项目多了,越来越能体会,传统数据库访问速度是限制系统性能的最大瓶颈。
而 Redis 基于内存的特性,可以极大地提高读写效率,使用得当,往往使系统性能有质的提升。
在项目开发中遇到访问频率非常高的热点数据时,可以优先考虑使用 Redis 进行存储操作。
什么是Redis
Redis是什么?
redis是一款高性能的NOSQL系列的非关系型数据库
Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库
Redis 非常适合作为热点数据的缓存
基于内存的键值型数据库
Redis(远程字典服务)
全称为:Remote Dictionary Server
Re di s(Remote Dictionary Server)
Redis->Remote Dictionary Server
Remote Dictionary Server(远程字典服务器)
Redis(Remote Dictionary Server )
远程字典服务
即远程字典服务
(远程字典服务器)
远程数据服务
Redis是C语言写的
基于C语言编写的
使用ANSI C语言编写
该软件使用C语言编写
C 语言编写的
纯C编码
key-value存储系统
可以存储键和五种不同类型的值之间的映射
Redis是的kv键值对数据库
Redis是一个key-value存储系统
Key-Value数据库
内存key-value缓存数据库
Redis是基于key-value操作的
基于内存,可持久化
可基于内存亦可持久化的日志型
内存中的数据结构存储系统
不同级别的 磁盘持久化(persistence)
存在内存中
Redis是一款内存高速缓存数据库。
内存key-value缓存数据库
可持久化
Redis跟memcache不同的是,储存在Redis中的数据是持久化的,断电或重启后,数据也不会丢失。
支持数据持久化
高速缓存数据库
1秒钟可以处理几万个请求
Redis能读的速度是110000次/s,写的速度是81000次/s
以设置和获取一个256字节字符串为例,它的读取速度可高达110000次/s,写速度高达81000次/s。
官方提供的数据QPS为100000+,完全不比同样是使用key-value的Memecache差
Redis以内存作为数据存储介质,所以读写数据的效率极高,远远超过数据库。
支持丰富的数据类型
值支持五种基本数据类型,三种特殊数据类型
Redis支持String、Hash、List、Set、sorted set等数据结构
字符串、列表、集合、有序集合、散列表。
- 字符串(string)
- 散列(hash)
- 列表(list)
- 集合(set)
- 有序集合(sorted set、zset)
- 散列(hash)
- 列表(list)
- 集合(set)
- 有序集合(sorted set、zset)
还支持BitMaps、HyperLogLog、GEO等数据结构
支持多种类型的数据结构
多种键值数据类型
Redis通过提供多种键值数据类型来适应不同场景下的存储需求
数据结构是专门进行设计的,对数据操作也简单
丰富的数据类型
支持多种数据结构
数据结构丰富
默认端口是6379
其实是因为作者把某个女星的姓名用九宫格打出来是6379
多种集群方案
支持数据备份、集群、哨兵
Redis支持主从模式,可以配置集群,这样更利于支撑起大型的项目,
这也是Redis的一大亮点
Redis和memcache有什么区别
Memcache
多线程,非阻塞IO
支持的数据类型只有String
键的类型只能为字符串
依靠客户端分片写入数据实现集群
支持value: 1MB
在memcached,通常需要把数据拿到客户端进行类似的修改再set回去,增加了网络io的次数和数据体积。
Redis
单线程,多路IO复用
持久性
支持value: 512MB
redis支持服务器端的操作
数据结构更多
相比于memcached而言,有更多的数据结构和支持更丰富的数据操作。
redis比memcache拥有的数据结构更多
集群模式
memcache原生不支持集群,redis支持cluster集群模式的。
memcached没有原生的集群模式,需要依靠客户端来实现往集群分片中写入数据,但是redis目前是原生支持cluster模式。
性能对比
redis是单线程(6.0多线程),memcache是多线程
由于redis只使用单核,memcached可以使用多核,所以平均每个核上redis存储小数据比memcached高,
redis在存储小数据时性能更高
但是100k以上的数据,memcached性能高于redis
在100k以上的数据中,memcache要高于redis
内存使用高效率
单纯的k-v是memcached高
但是采取hash结构的话,内存利用率是redis高
为什么要使用Redis , 不使用map/guava做缓存
map 或者 guava 实现的是本地缓存
本地缓存的特点
轻量以及快速
生命周期随着 jvm 的销毁而结束
在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
分布式缓存的特点
在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。
缺点
需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
高性能
第一次访问数据库慢
缓存后,访问直接操作内存,快
高并发
缓存数据库数据到redis,减少访问数据库
redis的高并发的实现:【本质就是读写分离】
首先先理解一个常识,单机的redis能够承载的qps是上万到几万不等,如果qps在10万+,那单机的redis肯定是会崩的。
所以处理处理高并发说白了就是分流,redis采取主从的模式,对于落到redis上的操作我们区分为读和写。
正常情况下,读操作的数量都是大于写的,所以我们把所有的读操作落在slave上,所有的写操作落在master上,然后master把数据同步到所有的slave上。
Redis作为数据库/缓存的区别
随着业务的运转,redis内存应该淘汰掉冷数据:
http://redis.cn/topics/lru-cache.html
http://redis.cn/topics/lru-cache.html
作为缓存的几点要求
+ 缓存数据不重要,如点赞数,评论数;
+ 不是全量数据
+ 缓存应该随着访问变化--热数据
+ 不是全量数据
+ 缓存应该随着访问变化--热数据
Redis优点
简单
1、Redis分为单机版和集群版
2、不依赖外部库
3、redis依赖于单线程模型
通讯协议
支持网络
RESP
Redis Serialization Protocal
支持多种编辑语言,提供多种语言的API
1、Java
2、Php
3、c
自己构建了VM 机制
Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
开源的
由意大利人Salvatore Sanfilippo(网名:antirez)开发的
高可用、分布式
性能方面
每秒可读11万次
每秒可写8万1千次
通过Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)
功能丰富
1、发布订阅
2、事务
支持事务,操作都是原子的
支持事务
事务(transactions)
3、Lua脚本与驱动事件
LUA脚本(Lua scripting)
LUA脚本
LRU驱动事件(LRU eviction)
LRU驱动事件
4、pipeline
节省网络开销
注意每次pipeline携带的数据量
注意m操作与pipeline的区别:原子 vs 非原子
pipeline每次只能作用在一个redis节点上
读写性能优异
支持主从复制
复制(replication)
主从复制
Redis缺点
基于内存,受物理内存限制
不具备自动容错和恢复,需手动
宕机,主机的部分数据未同步从机,导致数据不一致
难支持在线扩容,复杂
Redis的数据库
默认使用16个数据库,默认使用第0个
Redis一共有16个数据库
如图
Redis的16个数据库是相互独立的,可以使用select index进行切换
如图
可以使用select进行切换数据库
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> DBSIZE
(integer)0
清空Redis数据、清除数据库
flushdb
清除当前数据库的数据
清除当前数据库
flushall
清空所有数据库的记录
清空所有数据库
Redis可以设置过期时间
如图
测试性能
redis-benchmark是一个压力测试工具
进入usr/local/bin
简单测试
Redis官网
Redis中文
Redis英文
Redis的基本操作、数据类型、基础类型、数据结构
Redis基本数据类型
数据类型、基础类型、数据结构
数据结构
数据结构
基础数据结构
数据结构介绍
多样的数据类型
五种基础数据类型
Redis的数据类型
数据类型介绍
Redis基本数据类型
redis数据类型
基础数据类型
redis都有哪些数据类型?
分别在哪些场景比较合适?
分别在哪些场景比较合适?
数据操作
Redis其他操作
数据操作、基本操作
五种基础数据类型
图解5种数据类型
图解5种数据类型
字符串类型 String
String
string
String(字符串)
字符串(string)
String(就是类似java中的字符串操作)
存储字符串,整数,浮点数
存储简单字符串,json对象
string:字符串
字符串类型 String
字符串
string操作
基本字符串
内部编码
raw
raw
int
int
embstr
empstr
字符类型
k-V存储
可做简单的k-v缓存
操作命令、常用命令、相关命令
get、set、mset、mget
set
set user:id:name xiaoming
set user:id:age 20
setxx
设置值
存储: set key value
set [key] [value]
只能修改,必须存在key,才能存在
set key value [expiration EX seconds|PX milliseconds][NX|XX]
set k v
set k v
存放一个k-v对
如set lock1 1 EX 10 NX
set k v xx: key存在 才设置(update)
实战使用 set key:{id}:{filed}
get
get key
getset: 返回旧值
get k
获得k对应v
get [key]
取值
获取: get key
mget / mset
mset key value [key value ...]
mset k1 v1 k2 v2 k3 v3
mset k1 v1 k2 v2 ...
mset k1 v1 k2 v2 ...
mset 批量设置
存放多个k-v
批量添加记录
mget key [key ...]
mget 批量获得
mget k1 k2 k3
mget k1 k2 k3 ...
获得多个v
getset k v
getset key value
getset 先get再set
如果不存在值则返回null,再设置新值;若存在,返回原来的值,并设置新指
获取值之后 修改该K的V
setnx
setnx k v:
setnx k v
setnx key value
setnx (set if not exist) 不存在再设置
setnx [key] [value]
只能新建,如果存在key,则set失败
不存在才设置(add)
当库中有该K时不存。当库中没有改K时存放
非常重要(做分布式锁)
非常重要(做分布式锁)
setnx key xxx
若存在key,则将xxx设置进去
如果已经有了这个key,该操作无效
若不存在,则不会设置
如果key不存在就执行set创建(if not exists)
如果key不存在则设置key,返回1 否则什么都不做,返回0
如果不存在这个key,就存储
setex key seconds value
带过期时间的设置
setex (set with expire) 设置过期时间
setex key 30 xxx(30->指时间)
setex [key] [seconds] [value]
setex(set with expire)
ex:设置过期时间
限时存储: setex key 时间(s) value (类似限时激活码)
等价于 set+expire
setex k 60 v: 生存时间
msetex 批量设置过期时间
msetnx 批量判断是否存在
不存在设置
msetnx k1 v1 k4 v4
msetnx 是一个原子性的操作,要么一起成功,要么一起失败
可以批量添加,并且是原子操作
incr、incrby、decr、decrby、incrbyfloat
incr / decr / incrby / decrby
insc k1
该k1对应的v的值++(v必须是Integer类型)
inscby k1 步长
设置每次走的步长
descby k1 步长
DECRBY
设置每次走的步长
desc k1
该k1对应的v的值--(v必须是Integer类型)
数值、浏览量类型的字符串
incr key
+1操作
自增
incr view
incr key
INCR 自增
decr key
decr view
自减
DECR 自减
-1操作
指定-num操作
incrby key num
incrby view [num]
incrby key increment
incrbyfloat key 3.5
固定长度自增
增加key对应的值3.5
指定+num操作
INCRBY 设置步长,指定增量
decrby key num
decrby view [num]
固定长度自减
strlen、append
strlen
strlen [key]
查看长度
返回字符串的长度
取其字符集长度
append
追加值
APPEND 拼接
将value追加到旧到value
append [key] [value]
getrange key start end
getrange
getrange [key] [start] [end]
getrange [key] 0 -1就是完整字符串
GETRANGE key 0 2
GETRANGE 范围,截取字符串
截取字符串范围(闭区间)
获取字符串制定下标的所有值
setrange key index value
替换(相当于replace)
setrange
STRLEN 字符串长度
SETRANGE 替换指定位置的字符串
正反向索引:value第一个index=0,倒数第一个为-1,以此类推
设置执行下标所有对应的值
setrange [key] [start] [new]
capacity
底层数据结构、存储原理
redisObject
SDS
SDS, Simple Dynamic String
SDS
简单动态字符串(Simple Dynamic String)
扩容:< 1M 之前,加倍扩容; > 1M后,每次扩容1M
content:以字节\0结尾
len
redis底层是C,为什么不用c字符串,而用SDS
获取长度
C字符串并不记录自身长度,想获取长度只能遍历
sds直接获取len即可
内存分配
c字符串每次长度变化都会对数组进行内存重新分配,比较耗时
对sds内容进行修改或者需要扩展时,sds有空间预分配和惰性空间释放
缓冲区安全
C字符串不记录自身长度,不会自动进行边界检查,所以会增加溢出的风险
sds先检查空间是否满足修改所需的要求,如果不满足就先扩容再进行修改
二进制安全
C字符串是以空字符串(\0)结尾,所以字符串中不能包含空字符串,只能保存文本数据
既能保存文本数据,也能保存二进制数据(通过长度判断结束,不受影响)
应用场景
1、缓存
缓存 热点数据缓存
2、计数器
基于incr命令实现
计数器
3、分布式锁
基于setnx命令实现
分布式锁
统计多单位的数量
String类型使用场景
value除了是字符串还可以是数字
分布式数据共享
session共享
分布式session
全局ID
基于incrby命令实现
分布式ID生成(自增)
限流
基于incr命令实现
位统计
String/Byte
本质上讲在redis中,int类型用type key来看,也是string的;
但是object encoding key来看int类型key才会是int型;int型的key可执行incr等操作。
二进制安全
redis取值是字节流
redis取值是字节流
哈希类型 Hash
哈希类型 Hash
哈希表(hash)
hash:键值对
map格式
Hash
Hash(哈希)
Map集合,key-value键值对
hash:键值对
hash类型
hash操作
存储 对象
底层数据结构、内部编码、存储原理
dict
dict -> dictht[2] -> disctEntry
扩容时机:元素的个数等于第一维数组的长度;
bgsave时不扩容,除非达到dict_force_resize_ratio
扩容:扩容 2 倍
缩容:元素个数低于数组长度10%时
渐进式rehash
hashtable
哈希表
ziplist
ziplist
压缩列表
ziplist: 元素个数较小时,用ziplist节约空间
配置
hash-max-ziplist-entries
hash-max-ziplist-value
Hash操作、操作命令、常用方法、相关命令、常用命令
hget、hset、hmget、hmset
hget / hset name k v
hset myhash key1 value1
hset 单个设置值
hget
hget myhash key
hset、hmset
存值
存
hset key field value
hset [map] [key] [value]
存储: hset key field value
hset k field value
批量存值
存多个
hmset [map] [k1] [v1] [k2] [v2] ...
hmset 多个设置值 hmset myhash k1 v1 k2 v2
hmset k field value field value
hmset key field value [field value ...]
hget、hmget
取值
取
hget [map] [key]
hget key field
hget k filed
hget key field: 获取指定的field对应的值
批量取值
取多个
hmget [map] [k1] [k2] ...
hmget k field filed
hmget key field [field ...]
hmget myhash key1 key2 获取多个值
hsetnx name k v
hsetnx myhash key value
hsexnx [map] [key] [value]
hsetnx 如果不存在则设置,如果不存在者不设置
设置不存在的k-v键值对(如果没有就设置成功,如果已经存在就设置失败)
hgetall k
hgetall name
hgetall myhash
hgetall key
hgetall [map]
获取所有值
直接获取key下所有的hash
获取所有的field和value
取得、获得所有的k-v
hdecr 自减
hdecr
hdecrby
hkeys k
hkeys [map]
hkeys myhash
hkeys name: 返回所有key
hkeys 获取hash所有key
只获得所有的key
只取key
hvals k
hvals [map]
只取value
hvals name: 返回所有value
hvals myhash
hvals 获取hash所有值
只获得所有的value
hdel k filed
hdel
hdel key field
hdel myhash k1
删除
hdel 删除hash指定的key,对应的value也被删除
hexistsk filed
hexists [map] [key]
hexsits myhash key
hexists 判断hash中指定key是否存在
判断hash中key是否有field
判断key是否存在
hincr 指定增量
hincr
hincrby
hincrby / hincrbyfloat
hincrbyfloat
hincrby key field increment
对其value值做加减法操作
+ 使用场景:评论数、点赞数增加
hlen key
hlen [map]
hlen myhash
hlen 获取hash的size
获得k-v键值对数量
获取hash key field的数量
存储类型
value只能是字符串(字符串、整数、浮点数)
和string的差不多,只不过命令开头用h开头
相比于json串,可单独修改对象的字段
hash变更的数据 user name age ,尤其是用户信息子类的,经常变动的信息!
map 适合存储对象?
hash 更适合对象的存储,String更加适合字符串存储
因为对象的属性和值,可以认为是一个map集合里面的数据!
主要用来存放一些对象,后续操作的时候,可以仅仅修改对象里的某个值
value为hashmap构造
Hash
value为<key,value>,存对象,适合一key多value
ziplist
dict
应用场景
String的应用场景、Hash都可以做
缓存视频的基本信息(数据在mysql中)仿代码
记录网站每个用户个人主页的访问量
hset user:1:info pageview count
购物车
可以快速定位,存储的信息需要被频繁的修改可用hash存储,比如实现购物车
列表类型 List
列表类型 List
List、list
list数据类型
列表(list)
List(列表)
在redis里面, 可以把list完成 栈、队列、阻塞队列
所有list命令都是用L开头的
Redis不区分大小写命令
存储类型、存储原理、内部编码、底层实现、本质、底层数据结构
有序可重复
可重复
可重复集合
list:可重复集合
允许存在重复元素
可重复
支持重复元素
有序列表
有序的字符串
存储有序列表
链表结构
list 实际上是一个链表,
before node after,left,right都可以插入
如果key不存在,创建新的链表
如果key存在,新增内容
如果移除所有的值,形成空链表,也代表不存在
在两边插入或者改动值,效率最高!中间元素,相对来说效率较低写
list是一个双向链表、双端队列
双向链表
是个双向链表
是个双端队列
早期 :ziplist+linkedlist
元素少时用 ziplist
压缩列表(ziplist)
ziplist
压缩列表
当列表对象痛死满足两个条件时,
列表对象使用ziplist进行存储
列表对象使用ziplist进行存储
列表对象保存的元素数量小于512个
列表对象保存的所有字符串元素长度都小于64个字节
他将所有的元素紧挨着存储,分配的是一块连续的内存
元素多时用 linkedlist
linkedlist
linkedlist格式
后期:quicklist
3.2之前 ADList
一个通用的双端链表实现
3.2之后 quicklist
快速列表(quicklist)
quicklist
由于普通链表指针比较浪费空间且会加重内存碎片化,所以优化为quicklist
特点
将多个ziplist使用双向指针串起来(链表+ziplist)
既满足了快速的插入删除性能,又不会出现太大的空间冗余
List操作、操作命令、常用方法、相关命令、常用命令
exists [list]
判断list是否存在
lset
关于lset的用法
lset [list] [index] [value]
lset key index v
语法:lset key index value
Lset 将列表中指定下标的值替换成另一个值,更新操作
设置链表中指定位置为新值
替换节点值(把index下标的值用value替换,不存在就报错)
lset key 3 xxxx:接上条lpush数据
结果为 f e d xxxx b a
结果为 f e d xxxx b a
llen
llen
llen k
llen [list]
Llen
获取list长度
查看该队列的长度
获得list的长度
lpush、rpush、lpop、rpop
lpush
lpush k v
lpush [list] [value]
lpush list-name c b a
lpush key value:
LPUSH list one
lpush key value [value ...]
将元素加入列表左表
LPUSH 插入到第一个元素
LPUSH 将一个值或者多个值,插入到列表的头部(左-left)
lpush:l的意思是left,从左往右push;
+ lpush key a b c d e f在redis中存储的顺序为f e d c b a
+ lpush key a b c d e f在redis中存储的顺序为f e d c b a
从左边放
左存(头插)
rpush
rpush k v
rpush [list] [value]
rpush key value [value ...]
rpush key value
RPUSH list one
将一个值或者多个值,插入到列表的尾部(右-right)
从右边放
右存(尾插)
将元素加入列表右边
rpush:与lpush正好相反
列表类型 List
3. 删除:
:
:
:
:
rpop
rpop k
rpop key
rpop [list]
RPOP list
右取
与lpop正好相反
从右边取第一个
右边的值移除,移除list的最后一个元素
删除列表最右边的元素,并将元素返回
lpop
lpop k
lpop [list]
lpop key
LPOP list
左取
从左边取第一个
LPOP 移除第一个元素
左边的值移除,移除list的第一个元素
关于lpop的用法
语法:lpop key | 表示出栈/出队列中的第一个元素,并从原来的list集合中删除该元素
lpop:接上条lpush的数据;弹出数据为f
删除列表最左边的元素,并将元素返回
rpopLpush
rpoplpush list list2
rpoplpush list list2
rpoplpush source destination
移除列表最后一个元素,并且移动到新列表
blpop、brpop
brpop 带阻塞
brpop k timeout
brpop key timeout
从右边取,没取到的话阻塞timeout时间
List
blpop 带阻塞
blpop
blpop k timeout
blpop key timeout
从左边取,没取到的话阻塞timeout时间
等待元素出现(阻塞),第一个线程弹出
ltrim
关于ltrim的用法
语法: ltrim key start stop
ltrim [list] [start] [end]
ltrim key start end: 修剪
ltrim key start stop
Ltrim list 1 2
Ltrim 通过下标截取指定的长度
trim操作(把截取作为新的list替代旧的)
通过下标截取指定的长度,list改变,只剩下截取的元素
保留指定范围内的元素
保留区别的值
ltrim books 1 0 (这其实是清空了整个列表,因为区间范围长度为负)
linsert
linsert [list] [before | after] [value0] [value1]
linsert key before|after v new-v
linsert key before|after value newValue
Linsert list before/after value value1
插值(在value0 前 | 后 插入value1的值)
往list中value的位置before或after插入value1
在list指定的值前或者后插入newValue
Linsert 将某个具体的value插入到别表中某个元素的前面或者后面
lindex
关于lindex的用法
lindex [list] [index]
lindex key index
lindex queue 0
语法:lindex key index
Lindex list 1
返回链表中指定下标的值
根据index操作
Lindex 通过下标获得 list 中的某一个值
lrange
关于lrang的用法、range操作
语法:lrang start stop
lrange key start stop
lrange k 0 -1
lrange key start end: 范围
lrange [list] [start] [end]
lrange queue 0 -1
lrange key start end
获取:范围获取
0 -1 获取全部
lrange myList 0 -1
lrange myList 0 -1
查看队列
-1代表倒数第一个
-2 代表倒数第二个
实现分页
page size
(page-1)* size
page*size-1
LRANGE 获取范围内的值
LRANGE list 0 1 通过区间获取具体的值
LRANGE list 0 -1 查看列表中的值
情况一:0通常表示第一个元素 1则是表示第二个,依次类推
情况二:-1表示最后一个元素 -2则表示最后一个元素之前的元素,依次类推
out of rang indexs不是一个错误:
如果start > end: 返回一个empty of list
如果end > 实际的最后一个值,则默认返回最大的值。(end为实际的end)
lrange key 0 -1:接lpush数据
存在正负索引,第一个index为0,最后一个为-1
获取结果为f e d c b a
存在正负索引,第一个index为0,最后一个为-1
获取结果为f e d c b a
lrem
Lrem 移除固定的值
语法:lrem key count value
Lrem list 1(表示个数) one(表示对应的值)
lrem k count value
count = 0 ,移除队列里面所有与value 值相同的value
count > 0 , 从表头开始搜索,删除数据value的值,删除的个数为count个
count< 0 ,从表尾开始搜索,删除数据为value的值,删除的个数为 count的绝对值个
lrem key count v
count = 0: 删除所有v
count > 0: 从左到右删除count个
count < 0: 从右到左删除count个
lrem [list] [num] [value]
移除 某list num数量个 值为value的节点元素
移除 某list num数量个 值为value的节点元素
移除list集合中指定个数的value,精确匹配
移除指定的元素
删除count个值元素值为value的数据,返回被删除的元素数量。
*count>0 从头往后删除
count<0从尾往前删除
关于lrem的用法
相关tips、应用场景,可实现的功能
用做堆栈
+ 栈 同向命令
类似 栈
图示
分支主题
lpush
lrange
Lpush Lpop 先进先出
Lpush+Lpop=stack
lpush + lpop --> stack
示例
用做队列
+ 队列 反向命令
RPUSH
Lpush+rpop=queue
lpush + rpop --> queue
图示
分支主题
示例
消息队列
类似 消息队列
可做消息队列
简单的消息队列
Lpush Rpop 左近右出
lpush + brpop --> MQ
Lpush + Brpop=message queue
固定集合
Lpush+Ltrim=capped collection固定集合
lpush + ltrim --> capped collection
+ 数组
+ 阻塞,单播队列 FIFO
微博大V的那种粉丝列表
微博,某个大v粉丝,可以以list的格式放在redis里去缓存。
分页查询,基于微博的下拉分页
可以通过lrange,从某个元素开始读取多少个元素,分页查询。
实现高性能的分页
用户消息时间线
分页的坑
集合类型 Set
集合 Set
set
Set
集合类型 Set
Set(集合)
set的命令以s开头,set中的值不能重复,类似java里的方法;
Set与list区分,去重且无序
无序唯一
无序不重复
无序不重复集合
无序集合
无序集合
无序集合 最大40亿左右
自动去重
去重
不允许重复元素
set:不可重复集合
如果需要对一些数据进行快速去重,系统部署在多台服务器上,可以用redis set
底层数据结构、存储原理、存储类型、内部编码
dict
dict
IntSet
intset
intset
intset
整数数组 set-max-intset-entries 512
当元素都是整数并且个数较小时,使用 intset 来存储
hashtable
hashtable
哈希表
Set操作、操作命令、常用方法、相关命令、常用命令
sadd
sadd key member [member ...]
sadd key value
sadd k v
sadd key e
sadd [set] [member]
sadd myset value
添加记录
存储
在k的set 集合里面添加一个v,该v 不能重复
sadd set集合中添加元素
scard key
scard k
scard myset
scard [set]
scard 获取set集合中的内容元素个数
scard: 集合大小
查看指定set的元素个数
K的set集合的长度
spop
spop key
spop [set]
spop k
spop 随机移除set集合中的元素
随机移除一个元素
随机弹出一个
取出一个
smembers
smembers key
smembers key
smembers k
smembers [set]
SMEMBERS set
smembers myset
smembers: 所有元素,慎用
查看指定set的所有值
K的set集合的所有数据
SMEMBERS 查看指定set中所有的值
获取:获取set集合中所有元素
sismember key member
sismember k
SISMEMEBER myset value
sismember: 判断存在
判断key是否在集合中
SISMEMBER 判断某个值是否存在set集合中
srandmember
srandmember [set]
srandmember: 随机取元素
srandmember 随机抽选出set集合中元素
srandmember myset 随机抽选出set中一个元素
随机获取一个元素
srandmember [set] [num]
随机获取num个元素
从集合中随机挑num个元素
srandmember myset 2 随机抽选出指定个数的元素
集合sdiff / sinter / sunion
差集 SDIFF
SDIFF key1 key2
sdiff k1 k2 (k1-k2)
sdiff [set1] [set2]
找set1和set2中的差集
减集
交集 SINTER
sinter
sinter k1 k2
sinter key [key ...]
SINTER key1 key2
sinter [set1] [set2]
找set1和set2中的交集(共同关注可以这样实现)
交集
取交集
两个set的交集
sinterstore
sinterstore:取交集并且插入其交集
并集 SUNION
sunion
sunion k1 k2
sunion key [key ...]
SUNION key1 key2
sunion [set1] [set2]
找set1和set2中的并集
并集
取并集
sunionstore
sunionstore destination key [key ...]
srem
srem key e
srem myset value
srem [set] [member]
删除:srem key value:
移除指定set的指定值
srem 移除set集合中指定的值
删除set集合中的某个元素
smove
smove [oldset] [newset] [member]
从oldset移动指定的元素到newset
smove myset myset2 value 将指定value从myset移动到myset2中
将一个指定的值,移动到另外一个set集合
集合内实战
localhost:0>sadd user:1:follow it news his sports
"4"
localhost:0>smembers user:1:follow
1) "it"
2) "sports"
3) "his"
4) "news"
localhost:0>spop user:1:follow
"his"
localhost:0>smembers user:1:follow
1) "it"
2) "sports"
3) "news"
localhost:0>scard user:1:follow
"3"
localhost:0>sis
"ERR unknown command `sis`, with args beginning with: "
localhost:0>SISMEMBER user:1:follow entertainnments
"0"
localhost:0>
Set(无序不重复集合)
应用场景
抽奖
spop / srandmember --> random item
点赞、签到、打卡
商品标签
sadd --> tagging
商品筛选
sdiff(差集)、sinter(交集)、sunion(并集)
左交并差集、交集,并集,差集的操作
用户关注模型
两个大v的粉丝放在两个set,两个set做交集共同好友
用在共同关注,网站uv
sadd + sinter --> social graph
有序集合类型 Sorted Set/Zset
ZSet
ZSet
ZSet
zset
SortedSet
sorted set
Sorted Set/Zset
有序set
有序集合
排序的set
有序集合(zset)
zset特点
物理内存左小右大
hash: value -> score
权重/聚合指令
命令获取数据时,不随命令发生变化
集合操作
交集、并集
去重,但是可以排序
不允许重复元素,且元素有顺序
带score的有序集合
score
每个元素都有score
在set的基础上增加了一个值,
zset k1 score v1,score排序;不会重复
每个元素都会关联一个double类型的分数
Redis正是通过分数来为集合中的成员进行从小到大的排序。
随机层数
只需要调整前后节点指针
不止比较score
还会比较value
zset和set区别
1、都必须无重复元素
2、集合是无序的,有序集合是有序的
3、集合里面只有元素,有序集合里面有元素+游标
zset和list的区别
1、list可以有重复,zset无重复元素
2、list有序,zset有序
3、集合里面只有元素,有序集合里面有元素+游标
应用场景
成绩
积分
排行榜
最大特点是分数可以自定义排序规则
写进去时,给一个分数,自动根据分数排序,可以玩很多花样
写数据带分数,实现排行榜
用score排序,可做排行榜
用score排序,可做排行榜
存储类型、存储原理、内部编码、底层数据结构
ziplist
元素个数较小时,用ziplist节约空间
压缩列表
ziplist
skiplist
跳表
skiplist
skip list(跳跃表)
二分查找
插入跳表时,随机造层
本质上讲,就是牺牲存储空间,换取查询速度
skiplist+dict
dict
skip
常用命令、操作命令、zset操作、相关命令
zadd
zadd
添加
zadd k 分数 成员
zadd myzset 10
zadd myset k1 v1
zadd key score e
存储:zadd key score value
zadd [set] [score][member]
批量添加
添加 可批量添加
zadd k1 8 apple 2 banana 3 orange
8、2、3代表分值score
zadd key [NX|XX] [CH] [INCR] score member [score member ...]
zadd [set] [score1] [member1] [score2] [member2] ...
zscore
zscore key e
zscore myzset java 获取元素 score
zscore k1 apple
8
获取对应apple的分值
8
获取对应apple的分值
zrank
zrank key member
zrank k1 apple
2
获取对应apple的排序
2
获取对应apple的排序
zrem
zrem key e
zrem key value
zrem myset key
zrem [set] [member]
zrem myzset php cpp
zrem key leements
zrem key member [member ...]
zremrangebyrank key start end
zremrangebyscore key min-score max-score
删除元素、移除元素
zincrby
zincrby key score e
zincrby myzset 5 python 分值递增
zcard
zcard [set]
zcard key
zcard myzset
获得指定set的数量
zcard 获取有序集合的个数
返回元素的总个数
统计元素个数
zrange
zrange
zrank key value
获取第几
zrange k start end
排行 (从低到高)
zrange key start end [withscores]
zrange key start stop [WITHSCORES]
zrange myzset 0 -1 withscores 获取全部元素
zrange k1 0 -1
banana orange apple(已按照score排序存储)
获取:zrange key start end [withscores]
zrangebyscore
带score查询
zrangeByScore
zrangebyscore [set] [min] [max]
zrangeByScore k1 3 8 (获取分值从3到8)
orange apple
orange apple
zrangebyscore [set] [min] [max] withscores
zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
zrangebyscore k 分数的最小值 分数的最大值
zrangbyscore key min-score max-score
zrangebyscore myzset 20 30
指定分数区间排行(从低到高)
根据分值区间获取元素
按照score升序获得min到max区间的所有member
zrevrange
zreveage k start end
zrevrange key start stop [WITHSCORES]
zrevrange key 0 3
排行 (从高到低)
获取前几
zrevrangebyscore
zrevrangebyscore [set] [min] [max]
按照score降序获得min到max区间的所有member
Redis Zrevrangebyscore 返回有序集中指定分数区间内的所有的成员。有序集成员按分数值递减(从大到小)的次序排列。
具有相同分数值的成员按字典序的逆序(reverse lexicographical order )排列。
除了成员按分数值递减的次序排列这一点外, ZREVRANGEBYSCORE 命令的其他方面和 ZRANGEBYSCORE 命令一样。
具有相同分数值的成员按字典序的逆序(reverse lexicographical order )排列。
除了成员按分数值递减的次序排列这一点外, ZREVRANGEBYSCORE 命令的其他方面和 ZRANGEBYSCORE 命令一样。
zremrangebyscore
zremrangebyscore key min max
Redis Zremrangebyscore 命令用于移除有序集中,指定分数(score)区间内的所有成员。
zcount
zcount myset 0 3 获取指定区间的数量
zcount key min-score max-score
zcount myzset 20 60 根据分值统计个数
zinterstore / zunionstore
排序如何实现 ?
排序是怎么实现的?增删改查的速度?
按照默认属性score来排序,若score一致,则按照默认字典序排序
升序
ZrangeByScore myset -inf +inf 显示全部用户,从小到大
-inf 负无穷 +inf 正无穷
zrangebyscore myset min max withscores
ZrangeByScore myset -1 0 withscores
可以排序
降序
zrevrange
zrevrange myset max min
降序
三种特殊数据类型、其他数据类型、扩展类型
Geo 地理位置
Geo
geospatial
Geospatial
GEO
地理位置
地图信息分析
和地理位置相关
地理空间
地理信息定位
有效经度
-180到180
从-180度到180度。
有效纬度
-85到85
从-85.05112878度到85.05112878度。
使用场景
范围查询
通过范围查询得到相近编码值,在实际地理位置上也是相邻的
计算距离
推算出地理位置的信息
用于地理经纬度计算
两地之间的举例
共享位置
底层结构
type: zset
基于Sort Set
GEO底层是zset命令
底层实现是zset
可以使用zset的命令操作geo
使用zset的相关命令
六个命令、常用命令
GEOADD 添加地理位置
添加地理位置
geoadd:添加地理位置的坐标。
geoadd [key] [经度] [纬度] [名称] [经度] [纬度] [名称]..
GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
geoadd city 116.39 39.91 beijing 121.48 31.40 shanghai 113.27 23.15 guangzhou 113.88 22.55 shenzhen
GEOADD china:city px py name
添加:geoadd key longitude latitude member
添加到sorted set元素的数目,但不包括已更新score的元素。
GEOPOS 获取指定城市的经纬度
查看指定key的经纬度
获取:geopos key member
geopos:获取地理位置的坐标。
geopos [key] [名称] [名称] ...
geopos city beijing
GEOPOS Sicily Palermo Catania NonExisting
GEOPOS 命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。
当给定的位置元素不存在时, 对应的数组项为空值。
当给定的位置元素不存在时, 对应的数组项为空值。
GEODIST 两个城市之间的距离
两地之间的距离
geodist:计算两个位置之间的距离。
距离:geodist key member1 member2 [unit]
geodist [key] [地点1] [地点2] [ m | km | ft | mi ]
geodist city beijing shanghai km
GEODIST Sicily Palermo Catania 默认单位m
GEODIST Sicily Palermo Catania km
指定单位的参数 unit 必须是以下单位的其中一个:
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
计算出的距离会以双精度浮点数的形式被返回。 如果给定的位置元素不存在, 那么命令返回空值。
GEOHASH 返回一个或多个位置元素的 Geohash 表示
GeoHash
GeoHash
geohash
二分区间,区间编码
根据经纬度获取一个哈希值
geohash [key] [地点] [地点] [地点] ...
距离越近,获得的哈希值越接近
GEOHASH Sicily Palermo Catania
geohash:返回一个或多个位置对象的 geohash 值。
一个数组, 数组的每个项都是一个 geohash 。 命令返回的 geohash 的位置与用户给定的位置元素的位置一一对应。
该命令将返回11个字符的Geohash字符串,所以没有精度Geohash,损失相比,使用内部52位表示。返回的geohashes具有以下特性:
他们可以缩短从右边的字符。它将失去精度,但仍将指向同一地区。
它可以在 geohash.org 网站使用,网址 http://geohash.org/<geohash-string>。查询例子:http://geohash.org/sqdtr74hyu0.
与类似的前缀字符串是附近,但相反的是不正确的,这是可能的,用不同的前缀字符串附近。
返回值
一个数组, 数组的每个项都是一个 geohash 。 命令返回的 geohash 的位置与用户给定的位置元素的位置一一对应。
他们可以缩短从右边的字符。它将失去精度,但仍将指向同一地区。
它可以在 geohash.org 网站使用,网址 http://geohash.org/<geohash-string>。查询例子:http://geohash.org/sqdtr74hyu0.
与类似的前缀字符串是附近,但相反的是不正确的,这是可能的,用不同的前缀字符串附近。
返回值
一个数组, 数组的每个项都是一个 geohash 。 命令返回的 geohash 的位置与用户给定的位置元素的位置一一对应。
GEORADIUS 以给定的经纬度为中心, 找出某一半径内的元素
GEORADIUS Sicily 15 37 100 km
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
范围:georadius/georadiusbymember
范围可以使用以下其中一个单位:
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
m 表示单位为米。
km 表示单位为千米。
mi 表示单位为英里。
ft 表示单位为英尺。
withdist 显示到中间距离的位置
withcoord 显示他人的定位信息
georadius [key] [经度] [纬度] [半径] [单位] ...
georadius city 108 28 2000 km withcoord withdist count 2
GEORADIUS china:city 100 30 500 km withdis withcoord count 1 以100,30这个经纬度为中心,寻找方圆500km内的城市
可以搜索半径之内的所有城市
以一点为中心(给出经纬度),找出指定距离为半径的圆内地点(附近的人)
在没有给定任何 WITH 选项的情况下, 命令只会返回一个像 [“New York”,”Milan”,”Paris”] 这样的线性(linear)列表。
在指定了 WITHCOORD 、 WITHDIST 、 WITHHASH 等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。
在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:
以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
geohash 整数。
由两个元素组成的坐标,分别为经度和纬度。
举个例子, GEORADIUS Sicily 15 37 200 km WITHCOORD WITHDIST 这样的命令返回的每个子数组都是类似以下格式的:
["Palermo","190.4424",["13.361389338970184","38.115556395496299"]]
在指定了 WITHCOORD 、 WITHDIST 、 WITHHASH 等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。
在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:
以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
geohash 整数。
由两个元素组成的坐标,分别为经度和纬度。
举个例子, GEORADIUS Sicily 15 37 200 km WITHCOORD WITHDIST 这样的命令返回的每个子数组都是类似以下格式的:
["Palermo","190.4424",["13.361389338970184","38.115556395496299"]]
GEORADIUSBYMEMBER 找出位于指定范围内的元素,中心点是由给定的位置元素决定
GEORADIUSBYMEMBER Sicily Agrigento 100 km
georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
找出位于指定元素周围的其他元素
子主题
以一点为中心(使用zset中的member),找出指定距离为半径的圆内地点(附近的人)
georadiusbymember city beijing 3000 km withcoord withdist count 2
查看指定key已有地点
通过zset命令实现
zrange [key] 0 -1
移除地理位置
删除:zrem key member
zrem [key] [名称]
zrem city chengdu guangzhou
BitMaps位图
BitMaps
Bitmaps
Bitmap
bitmaps
BitMaps
bitmap
位图
数据结构
bit位,位存储
按位操作
type: string, 最大512M
用String类型实现的一种统计二值状态的数据类型
只有0和1两个状态
存01二值
操作二进制位进行记录
可以记录唯二状态的记录
常用命令
setbit
setbit
redis中每个字节占8位
setbit sign 0 0
setbit k offset v
setbit [map] [ 0 | 1 ]
添加操作
setbit key 1 1:代表着第一个字节的第1位赋值1,并转为ascli码值@
使用bitmaps记录一周七天,打卡上班的例子
周1:1 周二:0 周三:0......
周1:1 周二:0 周三:0......
getbit
getbit
getbit sign 3
getbit [map] [index]
查询操作
bitcount
统计操作
bitcount k [start end] 统计
bitcount [map]
bitcount sign 统计数量
bitop op destKey key1 key2 位运算
bitpos k targetBit [start] [end] 查找
bitfield操作多个位
使用场景
可用于统计打卡
HyperLogLog
Hyperloglogs
Hyperloglog
HyperLogLog
Hyperloglogs
hyperloglogs
hyperloglog
基数计算
基数
Hyperloglog基数计算
用于统计基数的数据集合类型
用于基数统计的算法
描述
极小空间完成独立数量统计
用于统计一个集合中不重复的元素个数(基数计数),
可以解决很多精确度要求不高的统计问题
type: string
特点、特性
内存占用小
占用空间小
超小内存唯一值计数
如果要从内存角度比较的化,Hyperloglog首选!
占用的内存是固定的,2^64不同的元素的技术,只需要费12kb内存!
有极小的误差
有误差
统计结果有误差
标准误差为0.81%
通过hash函数映射,并不存储元素本身
有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
无法查看指定key的已存入元素
缺点
有错误率 0.81%
不能返回单条元素
使用场景、应用场景
用于求不重复的元素个数
适用于uv浏览量统计
统计页面的UV
统计在线用户数
统计注册IP数
常用命令
命令(pf为创建该数据结构的人的名字缩写)
Pfadd
添加
命令将所有元素参数添加到 HyperLogLog 数据结构中
添加:pfadd key e1 e2...
pfadd [key] [member] [member] ...
pfadd key element [element ...]
PFADD key element [element ...] 返回值:整型,如果至少有个元素被添加返回 1, 否则返回 0。
redis 127.0.0.1:6379> PFADD mykey a b c d e f g h i j
(integer) 1
redis 127.0.0.1:6379> PFCOUNT mykey
(integer) 10
(integer) 1
redis 127.0.0.1:6379> PFCOUNT mykey
(integer) 10
Pfcount
统计数量
计数:pfcount key
pfcount [key]
pfcount key [key ...]
命令返回给定 HyperLogLog 的基数估算值。
PFCOUNT key [key ...] 返回值:整数,返回给定 HyperLogLog 的基数值,如果多个 HyperLogLog 则返回基数估值之和。
redis 127.0.0.1:6379> PFADD hll foo bar zap
(integer) 1
redis 127.0.0.1:6379> PFADD hll zap zap zap
(integer) 0
redis 127.0.0.1:6379> PFADD hll foo bar
(integer) 0
redis 127.0.0.1:6379> PFCOUNT hll
(integer) 3
redis 127.0.0.1:6379> PFADD some-other-hll 1 2 3
(integer) 1
redis 127.0.0.1:6379> PFCOUNT hll some-other-hll
(integer) 6
redis>
(integer) 1
redis 127.0.0.1:6379> PFADD hll zap zap zap
(integer) 0
redis 127.0.0.1:6379> PFADD hll foo bar
(integer) 0
redis 127.0.0.1:6379> PFCOUNT hll
(integer) 3
redis 127.0.0.1:6379> PFADD some-other-hll 1 2 3
(integer) 1
redis 127.0.0.1:6379> PFCOUNT hll some-other-hll
(integer) 6
redis>
Pfmerge(并集)
pfmerge [newkey] [key1] [key2] ...
PFMERGE destkey sourcekey [sourcekey ...]
pfmerge destkey sourcekey [sourcekey ...]
合并:pfmerge destKey sourceKey1 sourceKey2
合并多个集合形成一个新的集合
redis 127.0.0.1:6379> PFADD hll1 foo bar zap a
(integer) 1
redis 127.0.0.1:6379> PFADD hll2 a b c foo
(integer) 1
redis 127.0.0.1:6379> PFMERGE hll3 hll1 hll2
OK
redis 127.0.0.1:6379> PFCOUNT hll3
(integer) 6
redis>
(integer) 1
redis 127.0.0.1:6379> PFADD hll2 a b c foo
(integer) 1
redis 127.0.0.1:6379> PFMERGE hll3 hll1 hll2
OK
redis 127.0.0.1:6379> PFCOUNT hll3
(integer) 6
redis>
命令将多个 HyperLogLog 合并为一个 HyperLogLog ,合并后的 HyperLogLog 的基数估算值是通过对所有 给定 HyperLogLog 进行并集计算得出的
Streams
Streams
数据库操作、db的相关命令
select <dbid>:切换数据库
select index
切换库
flushdb:清空当前数据库
# 删除数据库
flushdb
flushdb
flushdb
清空当前库
dbsize:查看数据库数据个数
dbsize
查看当前库key的数量
flushall:清空所有数据库
flushall
清空所有库0-15(不安全)
# 删除所有数据库
flushall
flushall
move key index:将键值对从当前db移动到目标db
lastsave:获取最后一次持久化操作的时间
key常用命令操作、key的相关命令
keys * : 查询所有的键
Keys
keys *
获取当前库的所有key
# 查看所有key-value
key *
key *
type key:查看key对应值的类型
type key
type key
key的类型
获取键对应的value的类型
# 查看当前key的类型
type name
type name
del key:删除key
del k
删除key
del key [key ...]
del
删除: del key
del key:删除指定的key value
keys pattern:查看符合指定表达式的所有key,pattern可以为*,?等
exists key:查看key是否存在
exists k
判断是否存在key
exists key [key ...]
# 判断name是否存在
exists name xxx
exists name xxx
randomkey:随意选取key
expire key seconds:为键值设置过期时间
exprie
设置key的有效时间
expire key seconds
expire k seconds
# 设置name的过期时间
expire name 10
expire name 10
TTL key:查看key还有多久过期
ttl k
ttl key
查看key还有多久过期
查看key剩余的过期时间
# 查看剩余的过期时间
ttl name
ttl name
-1->永不过期,-2->已过期
-1表示key存在,没有过期时间
-2表示key不存在
rename key newkey:重命名key,强制执行
renamex key newkey:重命名key,非强制执行,如果newkey存在则语句不生效
persist k
去掉key的过期时间
# 将xxx从当前数据库移除
move name xxx
move name xxx
命令操作、Redis命令中心
http://www.redis.cn/commands.html
自定义数据类型
Redis基本对象结构 RedisObject
type 数据类型
值类型,五大基本类型
type
encoding 编码方式
编码方式,底层数据结构
encoding
ptr 数据指针
*ptr
数据指针
vm 虚拟内存
lru
记录上次访问时间,用于淘汰过期键值对
refcount
引用计数
其他
定义新类型和底层数据结构、在RedisObject中增加新类型的定义、开发新类型创建和释放函数、开发新类型的命令操作
内部底层数据结构与应用
Redis 对于某种常用数据结构,底层的存储方式至少有两种,以便于根据存储数据的实际情况使用合适的存储方式。
SDS - 简单动态字符串
简单动态字符串(SDS)
struct sdshdr{
int len; // 已用字节数量
int free; // 空闲字节数量
char []buff; // 用于保存字符串
}
int len; // 已用字节数量
int free; // 空闲字节数量
char []buff; // 用于保存字符串
}
空间预分配
若字符串长度小于1M,则多分配len长度空间,否则多分配1M空间
惰性空间释放
键值的底层都是SDS
AOF缓存区
记录本身长度 C需要遍历
修改字符减少内存重新分配
空间预支配
惰性空间释放
二进制安全
C只能保存文本数据 无法保存图片等二进制数据
sds是使用长度去判断
杜绝缓冲区溢出
兼容部分C字符串函数
SDS
ADList - 双向链表
双向链表
保存多个客户端的状态信息
列表订阅发布 慢查询 监视器
链表
dict - 字典
数据库 哈希键
Hash表节点
hash冲突用单向链表解决
渐进式 rehash
会逐渐rehash 新的键值对全部放到新的hash表
每个字典带 两个hash表
一个平时用 一个rehash时候用
字典
intset - 整数集合
整数集合
ziplist - 压缩表
压缩列表
压缩列表
连续内存,表头有zlbytes(列表长度),zltail(表尾偏移量),zlen(列表中entry个数)
便于查找首尾元素
为了节约内存,当数据量小,元素短时, list、hash、zset 采用压缩列表实现
配置项
zset-max-ziplist-entries 128 最大元素个数
zset-max-ziplist-value 64 元素最大长度
zset-max-ziplist-value 64 元素最大长度
list-max-ziplist-size -2(8kb)最大元素数量
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
hash-max-ziplist-value 64
entry结构
previous_entry_length(上一个节点的长度)
如果前一个数据项占用内存小于254字节,则用1字节表示
如果前一个数据项占用内存大于等于254字节,则用5字节表示
encoding(1字节)、content
quicklist - 快速列表(ADList + ziplist)
skiplist - 跳跃表
跳表
跳表节点
包含一个后退指针和多个前进指针
跳表以有序的方式在层次化的链表中保存数据,效率和平衡树媲美
在链表的基础上增加多级索引
O(logN), 32 层索引,0.25 概率选中
双向链表 ,平均查找效率为O(logN),表头、表尾、表长、最高层数的复杂度均为O(1);
Pub/Sub
键值用什么结构组织?
全局哈希表
哈希表其实就是一个数组,数组的每一个元素被称为哈希桶,哈希桶保存kv实体的指针
不同数据类型有相同的元数据需要记录,使用Redis Object结构统一记录元数据同时指向实际数据
Redis Object
int编码
存储整形值
元数据,int
embstr编码
字符串小于等于44字节
元数据,ptr,SDS 连续存储 避免内存碎片
row编码
字符串大于44字节
元数据,ptr,SDS独立空间
哈希桶
*key,*value,*next
解决哈希冲突
链式哈希,同一个哈希桶中的多个元素用一个链表来保存
哈希冲突链过长
rehash
分配一个更大空间的哈希表2,重新映射,释放哈希表1
由于涉及大量数据拷贝,采用 渐进式rehash
每处理一个请求,按序从哈希表1 rehash一个桶到哈希表2
没有新请求时定时执行rehash
装载因子=(load factor 哈希表大小/元素数量)>= 1 允许rehash,大于等于5时立即rehash
Redis应用场景、使用场景、案例、能做些什么
(1)缓存、分布式缓存
缓存
缓存是性能提升的大杀器!
内存的读写速度是硬盘的几十倍到上百倍。
缓存实际上就是利用内存的高速读写特性,提高热点数据的操作速度。
缓存系统
缓存
效率高,可以用于高速缓存
可以用作数据库、缓存
缓存(数据查询、短连接、新闻内容、商品内容等等)
Spring Boot 支持多种缓存实现方式
Spring Boot 支持多种缓存实现方式,可以根据项目需求灵活选择
Spring Boot 中使用缓存非常简单,并且支持多种缓存实现
缓存数据量较小的项目,可以使用 Spring Boot 默认缓存。
缓存数据量较大的项目,可以考虑使用 Ehcache 缓存框架。
如果是大型系统,对缓存的依赖性比较高,还是建议采用独立的缓存组件 Redis ,通过主备、集群等形式提高缓存服务的性能和稳定性。
Spring Boot 默认缓存
基于 ConcurrenMapCacheManager 缓存管理器
Spring Boot 默认缓存是基于 ConcurrenMapCacheManager 缓存管理器实现的
从这个类名就能发现,它本质上应该是一个 Map 集合容器。
ConcurrenMapCacheManager 结构比较简单,一般用于比较轻量级的缓存使用场景。
也就是缓存的数据量比较小,缓存操作不是特别频繁的场景。
Spring Boot 默认缓存实现过程
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-cache ,生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖和缓存依赖。
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
添加注解 @EnableCaching 开启缓存功能
在启动类上添加注解 @EnableCaching 开启缓存功能。
@SpringBootApplication
@EnableCaching // 开启缓存
public class SpringBootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootCacheApplication.class, args);
}
}
@EnableCaching // 开启缓存
public class SpringBootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootCacheApplication.class, args);
}
}
定义服务层方法
正常服务层方法会调用数据访问层方法访问数据库,此处我们只需要演示缓存的作用,所以打印日志代替数据库访问方法。
演示缓存的作用
//商品服务类
@Service
@CacheConfig(cacheNames = "GoodsCache")
public class GoodsService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
//按id获取商品信息
@Cacheable
public GoodsDo getById(Long id) {
logger.info("getById({})", id);
GoodsDo goods = new GoodsDo();
goods.setId(id);
goods.setName("goods-" + id);
return goods;
}
//删除商品
@CacheEvict(key = "#id")
public void remove(Long id) {
logger.info("remove({})", id);
}
//编辑商品信息
@CachePut(key = "#goods.id")
public GoodsDo edit(GoodsDo goods) {
logger.info("edit id:{}", goods.getId());
return goods;
}
}
@Service
@CacheConfig(cacheNames = "GoodsCache")
public class GoodsService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
//按id获取商品信息
@Cacheable
public GoodsDo getById(Long id) {
logger.info("getById({})", id);
GoodsDo goods = new GoodsDo();
goods.setId(id);
goods.setName("goods-" + id);
return goods;
}
//删除商品
@CacheEvict(key = "#id")
public void remove(Long id) {
logger.info("remove({})", id);
}
//编辑商品信息
@CachePut(key = "#goods.id")
public GoodsDo edit(GoodsDo goods) {
logger.info("edit id:{}", goods.getId());
return goods;
}
}
对于使用缓存的 GoodsService 服务类,具体解释下:
@CacheConfig 注解
用于指定本类中方法使用的缓存名称
该类使用的缓存名称为 GoodsCache
与其他缓存区域是隔离的。
@Cacheable注解
开启方法缓存,缓存的键是方法的参数,缓存的值是方法的返回值。
如果多次调用该方法时参数 id 值相同,则第一次会执行方法体,并将返回值放入缓存;
后续方法不会再执行方法体,直接将缓存的值返回。
@CachePut注解
可以更新缓存,key = "#id" 表示采用参数中的 id 属性作为键。
当缓存中该键的值不存在时,则将返回值放入缓存;当缓存中该键的值已存在时,会更新缓存的内容。
@CacheEvict 注解
可以移除缓存,当调用该方法时,会移除 goods 中 id 属性对应的缓存内容。
测试与验证
为了充分理解缓存的含义,我们通过测试类发起测试。
测试类代码
@SpringBootTest
class SpringBootCacheApplicationTests {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private CacheManager cacheManager;
@Autowired
private GoodsService goodsService;
// 显示当前使用的缓存管理器类型
@Test
void showCacheManager() {
// 输出:org.springframework.cache.concurrent.ConcurrentMapCacheManager
logger.info(cacheManager.getClass().toString());
}
// 缓存测试
@Test
void cacheTest() {
// 第一次执行,没有缓存,执行方法体
goodsService.getById(1L);
// 再次执行,直接取出缓存,不执行方法体
goodsService.getById(1L);
// 移除缓存
goodsService.remove(1L);
// 再次执行,已经没有对应缓存,所以执行方法体
GoodsDo oldGoods = goodsService.getById(1L);
// 打印缓存内容
logger.info("old goods id:{} name:{}", oldGoods.getId(), oldGoods.getName());
// 更新缓存
GoodsDo temp = new GoodsDo();
temp.setId(1L);
temp.setName("新的商品");
goodsService.edit(temp);
// 查询并打印已更新的缓存内容
GoodsDo newGoods = goodsService.getById(1L);
logger.info("new goods id:{} name:{}", newGoods.getId(), newGoods.getName());
}
}
class SpringBootCacheApplicationTests {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private CacheManager cacheManager;
@Autowired
private GoodsService goodsService;
// 显示当前使用的缓存管理器类型
@Test
void showCacheManager() {
// 输出:org.springframework.cache.concurrent.ConcurrentMapCacheManager
logger.info(cacheManager.getClass().toString());
}
// 缓存测试
@Test
void cacheTest() {
// 第一次执行,没有缓存,执行方法体
goodsService.getById(1L);
// 再次执行,直接取出缓存,不执行方法体
goodsService.getById(1L);
// 移除缓存
goodsService.remove(1L);
// 再次执行,已经没有对应缓存,所以执行方法体
GoodsDo oldGoods = goodsService.getById(1L);
// 打印缓存内容
logger.info("old goods id:{} name:{}", oldGoods.getId(), oldGoods.getName());
// 更新缓存
GoodsDo temp = new GoodsDo();
temp.setId(1L);
temp.setName("新的商品");
goodsService.edit(temp);
// 查询并打印已更新的缓存内容
GoodsDo newGoods = goodsService.getById(1L);
logger.info("new goods id:{} name:{}", newGoods.getId(), newGoods.getName());
}
}
查看下控制台输出如下,验证了设计的缓存机制。
使用 Spring Boot 默认缓存时控制台输出内容
使用 Ehcache 缓存
缓存框架
Spring Boot 默认的缓存实现比较简单,功能也十分有限。
如果是企业级的中大型应用,需要寻求更加稳定、可靠的缓存框架。
Ehcache 是 Java 编程领域非常著名的缓存框架
具备两级缓存数据——内存和磁盘,因此不必担心内存容量问题。
另外 Ehcache 缓存的数据会在 JVM 重启时自动加载,不必担心断电丢失缓存的问题。
总之 Ehcache 的功能完整性和运行稳定性远远强于 Spring Boot 默认的缓存实现方式,而且 Spring Boot 使用 Ehcache 非常便捷
接下来就来实现下。
添加 Ehcache 依赖
我们在 spring-boot-cache 项目的基础上添加 Ehcache 依赖。
<!-- Ehcache 依赖 -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<!-- cache-api 依赖 -->
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<!-- cache-api 依赖 -->
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
在 properties文件中指定配置文件的位置
spring.cache.jcache.config=classpath:ehcache.xml
spring.cache.type=jcache
spring.cache.type=jcache
添加 ehcache.xml 配置文件
Ehcache 的配置比较复杂
然后在 resource 文件夹中添加 ehcache.xml 配置文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">
<!-- 持久化路径 -->
<persistence directory="C://ehcache" />
<!--缓存模板 -->
<cache-template name="CacheTemplate">
<expiry>
<!--存活时间 -->
<tti>60</tti>
</expiry>
<resources>
<!--堆空间 -->
<heap unit="entries">2000</heap>
<!-- 堆外空间 -->
<offheap unit="MB">500</offheap>
</resources>
</cache-template>
<!--缓存对象 -->
<cache alias="GoodsCache" uses-template="CacheTemplate">
</cache>
</config>
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">
<!-- 持久化路径 -->
<persistence directory="C://ehcache" />
<!--缓存模板 -->
<cache-template name="CacheTemplate">
<expiry>
<!--存活时间 -->
<tti>60</tti>
</expiry>
<resources>
<!--堆空间 -->
<heap unit="entries">2000</heap>
<!-- 堆外空间 -->
<offheap unit="MB">500</offheap>
</resources>
</cache-template>
<!--缓存对象 -->
<cache alias="GoodsCache" uses-template="CacheTemplate">
</cache>
</config>
测试与验证
由于之前已经在启动类添加 @EnableCaching ,我们再次运行测试类,输出结果如下。
使用 Ehcache 时控制台输出内容
注意控制台出现了 EhcacheManager 的字样,说明我们此时使用的缓存是 Ehcache 。
使用 Redis 缓存
为什么还需要Redis缓存?
Ehcache 依然是 Java 进程内的缓存框架
受限于 JVM 整体的内存分配策略。
如果是大型系统,缓存的数据量特别大,且性能要求很高,可以考虑直接使用 Redis 作为缓存。
Redis 可以采用单机、主备、集群等模式,视乎具体项目需求决定即可。
目前各大云计算厂商均提供商用版的 Redis 缓存服务,性能卓越且接入简单快速。
华为云提供的缓存服务
本节简单地演示 Spring Boot 中使用 Redis 单机缓存的方法,真实生产环境中建议至少使用主备类型的 Redis 实例。
修改缓存依赖
因为需要使用 Redis 缓存,所以将引入的依赖项修改如下:
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
修改缓存配置
修改 application.properties 配置文件,将 Redis 配置及缓存配置设置如下:
# 过期时间
spring.cache.redis.time-to-live=6000s
# Redis库的编号
spring.redis.database=0
# Redis实例地址
spring.redis.host=127.0.0.1
# Redis实例端口号,默认6379
spring.redis.port=6379
# Redis登录密码
spring.redis.password=Easy@0122
# Redis连接池最大连接数
spring.redis.jedis.pool.max-active=10
# Redis连接池最大空闲连接数
spring.redis.jedis.pool.max-idle=10
# Redis连接池最小空闲连接数
spring.redis.jedis.pool.min-idle=0
spring.cache.redis.time-to-live=6000s
# Redis库的编号
spring.redis.database=0
# Redis实例地址
spring.redis.host=127.0.0.1
# Redis实例端口号,默认6379
spring.redis.port=6379
# Redis登录密码
spring.redis.password=Easy@0122
# Redis连接池最大连接数
spring.redis.jedis.pool.max-active=10
# Redis连接池最大空闲连接数
spring.redis.jedis.pool.max-idle=10
# Redis连接池最小空闲连接数
spring.redis.jedis.pool.min-idle=0
测试验证
由于之前已经通过注解 @EnableCaching 开启了缓存功能,此时我们直接运行测试类进行测试,输出结果如下:
使用 Redis 缓存时控制台输出内容
从上图输出结果可以看出,已经成功使用了 Redis 缓存管理器。
另外直接使用 Redis 客户端查看生成的缓存信息,如下图已经有名为 GoodsCache::1 的缓存键存在了。
Redis 客户端查看缓存信息
(2)消息中间件、消息队列、消息中间件MQ
Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。
虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。
Redis和RabbitMQ的入队和出队操作对比
各执行100万次,每10万次记录一次执行时间。
测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。
实验表明
入队
当数据比较小时,Redis的性能要高于RabbitMQ
如果数据大小超过了10K,Redis则慢的无法忍受
出队
无论数据大小,Redis都表现出非常好的性能
而RabbitMQ的出队性能则远低于Redis。
发布订阅PubSub
PubSub
publish channel-name msg
subscribe/unsubscribe channel-name
订阅者存放在一个链表中,发布者发布消息后,根据频道的key推送给该频道的订阅者
角色
发布者
订阅者
频道(channel)
模型
发布订阅和消息订阅对比
发布订阅系统
(3)网站的访问次数、网站访问统计
原理说明
大型网站访问次数的查询、更新非常频繁,
如果通过关系数据库读写,无疑会耗费大量的性能,
而使用 Redis 可以大幅提高速度并降低对关系数据库的消耗。
使用 Spring Initializr 创建项目
Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-redis
生成项目后导入 Eclipse 开发环境。
引入项目依赖
引入 Web 项目依赖与 Redis 依赖。
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置 Redis 数据库连接
修改 application.properties 配置文件内容如下。
# Redis库的编号
spring.redis.database=0
# Redis实例地址
spring.redis.host=127.0.0.1
# Redis实例端口号,默认6379
spring.redis.port=6379
# Redis登录密码
spring.redis.password=Easy@0122
# Redis连接池最大连接数
spring.redis.jedis.pool.max-active=10
# Redis连接池最大空闲连接数
spring.redis.jedis.pool.max-idle=10
# Redis连接池最小空闲连接数
spring.redis.jedis.pool.min-idle=0
spring.redis.database=0
# Redis实例地址
spring.redis.host=127.0.0.1
# Redis实例端口号,默认6379
spring.redis.port=6379
# Redis登录密码
spring.redis.password=Easy@0122
# Redis连接池最大连接数
spring.redis.jedis.pool.max-active=10
# Redis连接池最大空闲连接数
spring.redis.jedis.pool.max-idle=10
# Redis连接池最小空闲连接数
spring.redis.jedis.pool.min-idle=0
开发网站访问统计服务类
开发网站访问统计服务类,在第 1 次获取访问次数时初始化次数为 0 ,后续每次访问次数加 1 。
//网站访问统计服务类
@Service
public class VisitService {
// 设定访问次数Redis键名
private final static String KEY = "visit_count";
// 注入redisTemplate操作Redis
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 获取当前访问次数
public String getCurrentCount() {
String count = redisTemplate.opsForValue().get(KEY);
if (count == null || "".equals(count)) {
redisTemplate.opsForValue().set(KEY, "0");
return "0";
}
return count;
}
// 访问次数加1
public void addCount() {
redisTemplate.opsForValue().increment(KEY, 1);
}
}
@Service
public class VisitService {
// 设定访问次数Redis键名
private final static String KEY = "visit_count";
// 注入redisTemplate操作Redis
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 获取当前访问次数
public String getCurrentCount() {
String count = redisTemplate.opsForValue().get(KEY);
if (count == null || "".equals(count)) {
redisTemplate.opsForValue().set(KEY, "0");
return "0";
}
return count;
}
// 访问次数加1
public void addCount() {
redisTemplate.opsForValue().increment(KEY, 1);
}
}
并发访问测试
我们通过测试类发起并发访问测试,代码如下:
/**
* 访问统计服务测试
*/
@SpringBootTest
class VisitServiceTest {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private VisitService visitService;
@Test
void test() {
logger.info("访问次数:{}", visitService.getCurrentCount());
// 使用线程池快速发起10000次访问
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
cachedThreadPool.execute(new Runnable() {
public void run() {
visitService.addCount();
}
});
}
}
}
* 访问统计服务测试
*/
@SpringBootTest
class VisitServiceTest {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private VisitService visitService;
@Test
void test() {
logger.info("访问次数:{}", visitService.getCurrentCount());
// 使用线程池快速发起10000次访问
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
cachedThreadPool.execute(new Runnable() {
public void run() {
visitService.addCount();
}
});
}
}
}
此时我们通过 Redis 客户端发现 visit_count 的值如下:
并发访问测试结果
Redis 中的操作都是原子性的,要么执行,要么不执行,在高并发场景下依然可以准确的进行计数,关键是速度还非常之快!
(4)热门商品排行榜、应用排行榜
排行榜
原理说明
如果是大型网站,时刻有很多用户在访问网页,对热门商品排行榜的访问频率是非常恐怖的。
可以通过定时器,定时从关系数据库中,取出热门商品数据放入 Redis 缓存,
用户访问网页时,直接从Redis 缓存中获取热门商品数据。
这将大大提高响应速度,并降低对关系数据库的性能损耗。
定义商品类
简单的定义一个商品类,便于展现商品排行榜数据。
//商品类
public class GoodsDo {
//商品id
private Long id;
// 商品名称
private String name;
// 商品价格
private String price;
//商品图片
private String pic;
// 省略get set方法
}
public class GoodsDo {
//商品id
private Long id;
// 商品名称
private String name;
// 商品价格
private String price;
//商品图片
private String pic;
// 省略get set方法
}
开发商品排行榜服务类
开发商品排行榜服务类,负责从数据库查询最新排行榜信息,并更新到 Redis ,以及从 Redis 中取出排行榜信息。
//商品排行榜服务类
@Service
public class GoodsRankService {
// 设定商品排行榜Redis键名
private final static String KEY = "goods_rank";
// 注入redisTemplate操作Redis
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 更新Redis缓存的排行榜
public void updateRankList() throws JsonProcessingException {
// 此处直接定义商品排行榜,真实场景应为从数据库获取
List<GoodsDo> rankList = new ArrayList<GoodsDo>();
GoodsDo goods = new GoodsDo();
goods.setId(1L);
goods.setName("鸡蛋" + new Date());// 添加时间信息,以便测试缓存更新了
rankList.add(goods);
// 将rankList序列化后写入Reidis
ObjectMapper mapper = new ObjectMapper();
redisTemplate.opsForValue().set(KEY, mapper.writeValueAsString(rankList));
}
// 获取Redis缓存的排行榜
public List<GoodsDo> getRandkList() throws JsonMappingException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(redisTemplate.opsForValue().get(KEY), List.class);
}
}
@Service
public class GoodsRankService {
// 设定商品排行榜Redis键名
private final static String KEY = "goods_rank";
// 注入redisTemplate操作Redis
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 更新Redis缓存的排行榜
public void updateRankList() throws JsonProcessingException {
// 此处直接定义商品排行榜,真实场景应为从数据库获取
List<GoodsDo> rankList = new ArrayList<GoodsDo>();
GoodsDo goods = new GoodsDo();
goods.setId(1L);
goods.setName("鸡蛋" + new Date());// 添加时间信息,以便测试缓存更新了
rankList.add(goods);
// 将rankList序列化后写入Reidis
ObjectMapper mapper = new ObjectMapper();
redisTemplate.opsForValue().set(KEY, mapper.writeValueAsString(rankList));
}
// 获取Redis缓存的排行榜
public List<GoodsDo> getRandkList() throws JsonMappingException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(redisTemplate.opsForValue().get(KEY), List.class);
}
}
通过定时器更新排行榜
为启动类添加 @EnableScheduling 注解,以便开启定时任务,然后编写 RankListUpdateTask 类定时刷新排行榜。
//排行榜更新任务
@Component
public class RankListUpdateTask {
@Autowired
private GoodsRankService goodsRankService;
//容器启动后马上执行,且每1秒执行1次
@Scheduled(initialDelay = 0, fixedRate = 1000)
public void execute() throws InterruptedException, JsonProcessingException {
goodsRankService.updateRankList();
}
}
@Component
public class RankListUpdateTask {
@Autowired
private GoodsRankService goodsRankService;
//容器启动后马上执行,且每1秒执行1次
@Scheduled(initialDelay = 0, fixedRate = 1000)
public void execute() throws InterruptedException, JsonProcessingException {
goodsRankService.updateRankList();
}
}
开发控制器方法
需要一个控制器方法,用于演示获取商品列表的结果。
@RestController
public class GoodsRankController {
@Autowired
private GoodsRankService goodsRankService;
@GetMapping("getRankList")
public List getRankList() throws Exception {
return goodsRankService.getRandkList();
}
}
public class GoodsRankController {
@Autowired
private GoodsRankService goodsRankService;
@GetMapping("getRankList")
public List getRankList() throws Exception {
return goodsRankService.getRandkList();
}
}
测试
运行启动类,然后访问 http://127.0.0.1:8080/getRankList ,结果如下:
[{"id":1,"name":"鸡蛋Thu May 28 22:47:33 CST 2020","price":null,"pic":null}]
稍等会再次访问,结果如下:
[{"id":1,"name":"鸡蛋Thu May 28 22:48:09 CST 2020","price":null,"pic":null}]
说明设计的缓存机制生效了。
(5)分布式锁
获取锁时,
使用setnx加锁,锁的value值为一个随机生成的UUID,在释放锁时进行判断。
并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁。
setnx的使用(SETNX key val)
当且仅当key不存在时,set一个key为val的字符串,返回1
若key存在,则什么都不做,返回0
获取锁时
调用setnx,如果返回0,则该锁正在被别人使用,返回1则成功获取锁。
还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
释放锁时
通过UUID判断是不是该锁,
若是该锁,则执行delete进行锁释放。
先用setnx(set if not exists)抢锁,抢到之后再用expire命令给锁加一个过期时间,防止忘记释放锁
如果setnx之后expire之前进程意外结束或者需要重启,需要把setnx和expire合成一条命令来执行(保证原子性): set key value ex 5 nx
分布式锁
命令
setnx + expire
set xx ex 5 nx
集群问题
Redlock算法
常用
小扩展redlock(不算太重要)
(6)分布式会话
分布式集群架构中的session分离
分布式会话
(7)计数器、限流
计时器、计数器(浏览量) 设计
计时器、计数器
令牌
定时push
然后leftpop
问题
空轮训
blpop
空连接异常
重试
漏桶 funnel
make_space 灌水之前调用漏水 腾出空间 取决于流水速率
Hash
原子性有问题
Redis-Cell
redis cell
漏斗限流: redis-cell模块
cl.throttle key capacity count period 1
简单限流: zset实现滑动时间窗口
key: clientId-actionId
value: ms
score: ms
setnx ex
zset
窗口滑动
zset会越来越大
(8)实时系统
需要实时读写数据库时,例如评论数、点赞数
(9)短信验证
手机验证码
手机验证码
短信校验是常用到的一项保证安全的,短信的验证码都是随机产生的,
而且信息都是有时间限制的,一旦过了那个时间的有效期,验证信息就会自动删除。
图例
分支主题
代码
(10)布隆过滤器(Bloom Filter)
布隆过滤器
描述
可以用于检索一个元素是否在一个集合中
命令操作
bf.add / bf.madd
bf.add
bf.madd
bf.exists / bf.mexists
bf.exists
bf.mexists
原理
参数
m 个二进制向量
n 个预备数据
k 个哈希函数
构建
n个预备数据,分别进行k个哈希,得出offset,将相应位置的二进制向量置为1
判断
进行k个哈希,得出offset,如果全为1,则判断存在
特性
有一定的误差
当布隆过滤器说某个值存在时,这个值可能不存在;当它说某个值不存在时,那就肯定不存在
误差率
与 k (哈希函数)个数成反比
与 n (预备数据)个数成正比
与 m (二进制向量)长度成反比
应用场景
爬虫过滤已抓到的url就不再抓
邮件系统的垃圾邮件过滤功能
新闻客户端推荐系统去重
社交网络
聊天室的在线好友列表
检索某个key是否存在
搜索key
keys
scan
keys pattern
keys 没有offset,limit,一次性找出所有符合条件的key
keys算法是遍历算法,redis是单线程,顺序执行所有指令,其他指令必须等到keys指令执行完了才能继续执行,会导致redis服务卡顿
scan cursor [MATCH pattern] [COUNT count]
不会阻塞线程
提供limit参数,可以控制每次返回结果的最大条数
返回的结果可能会有重复,需要客户端去重
遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的
单次返回的结果是空的并不意味着遍历结束,而是要看返回的游标值是否为0
任务队列
秒杀
抢购
12306
异步队列
list结构作为队列,rpush生产消息,lpop消费消息(先进先出:队列),当lpop没有消息时,要适当sleep一会再试,
缺点:空轮询浪费资源
解决:用blpop/brpop, 即是阻塞读(blocking)
延时队列
sortedset,将消息序列化成一个字符串作为value,这个消息的到期处理时间作为score,然后用zrangebyscore指令轮询获取到期的任务进行处理
lpush / rpush
rpop / lpop -> brpop / blpop
应用场景
订单超过30分钟未支付,自动关闭
订单完成后, 如果用户一直未评价, 5天后自动好评
会员到期前15天, 到期前3天分别发送短信提醒
音频仿真体验者角色15天有效期,到期前1天发邮件提醒
Stream
数据过期处理(可以精确到毫秒)
商品超卖、秒杀富余
使用乐观锁解决
秒杀
秒杀富余
使用lua脚本解决
添加缓存的逻辑的原则:缓存逻辑不能影响正常的业务逻辑执行
众多语言都支持Redis,因为Redis交换数据快,所以在服务器中常用来存储一些需要频繁调取的数据,
这样可以大大节省系统直接读取磁盘来获得数据的I/O开销,更重要的是可以极大提升速度。
拿大型网站来举个例子,比如a网站首页一天有100万人访问,其中有一个板块为推荐新闻。
要是直接从数据库查询,那么一天就要多消耗100万次数据库请求。
上面已经说过,Redis支持丰富的数据类型,所以这完全可以用Redis来完成,将这种热点数据存到Redis(内存)中,
要用的时候,直接从内存取,极大的提高了速度和节约了服务器的开销。
Redis的单线程为什么速度这么快?
具体的问题如下
Redis的单线程为什么这么快?
为什么Redis性能高?
为什么redis单线程效率那么高
redis是单线程,为什么这么快
redis为什么单线程还这么快?
Redis为什么这么快
Redis其实是单线程的(核心)
Redis其实是单线程的
Redis是单线程的
Redis是单进程单线程模型
关于单线程的误区
1、高性能的服务器一定是多线程
解释:并不是多线程的性能就一定高
2、多线程(CPU上下文切换)一定比单线程效率高
解释:cpu上下文切换也需要消耗性能,这种切换一旦过多,就会造成资源浪费
单线程的缺点
无法发挥多核cpu的性能
可以通过多开Redis实例完善
Redis是非关系型数据库,数据与数据之间没有联系
单线程的注意事项
1、一次只运行一条命了
2、拒绝长命令或者慢命令
3、redis并不是所有操作都是单线程
fysnc
close
为什么需要多线程
平衡计算机硬件之间的差异
比如cpu要到磁盘读取数据,cpu就需要等待磁盘将数据
放入内存 ,这段时间很长,多线程的话CPU就不需要等待
,直接去处理其他的请求,等到磁盘的数据到内存中,再来处理
该数据
放入内存 ,这段时间很长,多线程的话CPU就不需要等待
,直接去处理其他的请求,等到磁盘的数据到内存中,再来处理
该数据
采用单线程的好处
避免了不必要的上下文切换
避免线程切换和竞态消耗
避免了多线程的频繁上下文切换
避免了线程的切换
避免了多线程的频繁上下文切换问题
对于内存来讲,如果没有上下文切换效率是最高的
什么是上下文切换问题?
多线程(CPU上下文切换:耗时操作)
线程间切换,一个线程让出处理器使用权,就是”切出“;另外一个线程获取处理器使用权,就是切入。
在切入切出过程中,操作系统会保存和恢复相关的进度信息。
这个进度信息就是常说的“上下文”,
上下文中一般包含了
寄存器的存储内容
程序计数器存储的指令内容
避免了不必要的竞争条件
避免锁
Redis的读写操作是单线程的
读写速度快
与传统关系型数据库相比,它最大的优势就是读写速度快。
Redis到底有多快?
使用 Windows 版本的 Redis 进行过真实测试,每秒读写次数均可以超过1 万次。
据了解, Redis 每秒的读写操作次数其实是可以达到 10 万多次的。
官方提供测试数据,50个并发执行100000个请求,读的速度是110000次/s,写的速度是81000次/s
多次读写都在一个CPU上操作的,在内存情况下,单线程就是最佳方案
这里的单线程只是在网络IO和键值对读写上是单线程,其他处理还是会使用多线程,例如AOF
Redis并不是所有操作都是单线程
fysnc
close
完全基于内存,纯内存操作
纯内存操作
redis的大量操作是在操作内存
内存数据库,速度快,支持数据持久化
纯内存
纯内存 k/v
Redis的数据保存在内存中
将所有数据放入内存中的,所以使用单线程操作效率就是最高的
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
文件事件分派器和三个处理器都是基于纯内存快速处理请求,速度非常快。微秒级。
redis是基于内存操作的,CPU不是redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽
它基于内存操作,它的性能瓶颈其实是内存大小,频率,以及网络带宽
使用多路I/O复用模型,非阻塞I/O
Redis里面是单线程,想支持并发连接怎么办
想使用一个单线程去支持高并发,那么线程就不能阻塞
让一个线程去管理多个连接
io多路复用技术
IO多路复用,非阻塞I/O
核心的nio的io多路复用
使用多路I/O复用模型
多路复用技术
多路复用技术
io多路复用
非阻塞IO
核心是非阻塞IO多路复用机制
IO多路复用程序,只负责轮训所有连接的socket,监听说有socket产生的请求,然后压到队列里面去。
非阻塞是指IO多路复用程序只把请求压到队列,不进行处理。
实现同时对多个文件描述符(fd)进行监控,一旦某个描述符就绪,就通知程序进行操作
服务端采用单线程,当 accept 一个请求后,在 recv 或 send 调用阻塞时,将无法 accept 其他请求(必须等上一个请求处理 recv 或 send 完 )
多路复用中的两种队列
指令队列
响应队列
epoll事件轮询API
int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程
Redis的持久化模式
什么是持久化
把内存的数据写到磁盘中,防止宕机内存的数据丢失
什么是Redis持久化
Redis持久化
redis持久化
Redis持久化
Redis如何避免数据丢失?
redis的所有数据都保存在内存中,对数据的更新将异步地保存到磁盘中
内存存储,持久化,内存中是断电即失,所以持久化很重要 AOF,RDB)
Redis的存储分为三部分
内存存储
磁盘存储
log文件
为什么要进行持久化操作?持久化的意义
redis是一个内存数据库
Redis是内存数据库,断电就会丢失所有数据,所以必须有持久化操作
当redis服务器重启,获取电脑重启,数据会丢失,
可以将redis内存中的数据持久化保存到硬盘的文件中。
Redis所有的数据保存在内存中,对数据的更新将异步地保存到磁盘中
Redis提供类RDB和AOF进行持久化操作
重启后,Redis可以从磁盘重新将数据加载到内存中,这些可以通过配置文件对其进行配置
故障恢复,比如部署了redis作为缓存,保存了一些重要的数据。
持久化主要做灾难恢复数据恢复,也可以归类到一个高可用的环节里面去
持久化+备份
通过持久化将数据写到磁盘上,然后把磁盘上的数据备份到云存储上去。保证数据不会丢失
Redis持久化数据和缓存怎么扩容
使用一致性哈希实现动态扩容缩容
必须使用固定的keys-to-nodes映射关系,节点一旦确定不能变化
如果还需要动态化,会需要将之前的数据进行再平衡操作,增加复杂性
内存管理
设置内存上限
一般预留 30%
config set maxmemory 6GB
config rewrite
动态调整内存上限
内存回收策略、内存放不下怎么办
内存回收
无法保证立即回收已经删除的 key 的内存
flushdb
设置过期时间
setex key seconds value
过期键的删除策略
三种过期键的删除策略
定时删除、定时过期
定时扫描
定时过期(主动淘汰)
内存友好,占用cpu资源去处理
消耗内存
创建一个定时器
每个过期时间都需要一个定时器,过期清理
定时删除的原理:
给每一个设置了过期时间的key分配一个线程,该线程睡眠了过期时间后,起来删除该key
缺点:当过期的key特别多时,创建的线程也会非常多,特别消耗cpu资源
每秒运行 10 次,采样删除
每隔默认100s随机检查设置了过期时间的key,若已过期则删除
慢模式:随机检查 20 个key
如果超过25%的key过期
循环执行:快模式??
否则退出
Redis每秒10次做的事情
1.测试随机的20个keys进行相关过期检测。
2.删除所有已经过期的keys。
3.如果有多于25%的keys过期,重复步骤1。
算法默认不会超过25ms的扫描时间
2.删除所有已经过期的keys。
3.如果有多于25%的keys过期,重复步骤1。
算法默认不会超过25ms的扫描时间
问题:
如果大量key同一时间失效,会造成循环多次,直至过期key变得稀疏(1/4以下),才会停止。频繁回收内存页,会产生一定CPU损耗,客户端读写请求卡顿。
解决方案:
大量key过期时间不能设置为同一时间点,尽量设置为随机数+时间点!
+ 注:从库是不会进行过期扫描的,由主库生成del语句,进行淘汰key操作。
+ 注:从库是不会进行过期扫描的,由主库生成del语句,进行淘汰key操作。
定期删除、定期过期
每隔一定时间去扫描库中expires字典一定数量的key,清除
定期删除原理:专门有一个线程监视所有设置了过期时间的key的时间,
如果过期,将该key删除
如果过期,将该key删除
redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期,过期则删除。
定期删除是随机抽取一部分并不是全部,所以可能会导致很多过期key到了时间没有被删除掉。
定期删除是随机抽取一部分并不是全部,所以可能会导致很多过期key到了时间没有被删除掉。
检查 删除 但是是随机的
缺点:实时性差一点
惰性删除、惰性策略、懒惰删除、惰性过期
可能存在大量key
访问该key,才会判断是否过期,节省cpu,耗内存
极端下会出现大量过期的key,而不会被清除,占用内存
当你去get一个key的时候,redis会检查一下这个key是否过期了,如果
过期了,那么就删除,这就是惰性删除。
过期了,那么就删除,这就是惰性删除。
惰性删除原理:当用户访问该key 时,会判断该key 是否过期了
如果过期了就删除该key,给用户返回null,如果没有过期就返回value
如果过期了就删除该key,给用户返回null,如果没有过期就返回value
缺点:如果用户一直不访问该key,它就一直不会删除,
会一直占用内存
会一直占用内存
访问key
expired dict
del key
惰性过期(被动淘汰)
在客户端访问key时再进行检查如果过期了就立即删除
unlink指令针对于del的key是一个非常大的对象,延迟删除,将其交给后台异步线程操作。
访问key时才检查是否过期,过期则删除更新
del -> unlink
flushdb -> flushdb async
Redis中同时使用了两种过期策略
(Redis默认)
可以互相解决缺点
惰性过期和定期过期
Redis使用的过期键删除策略- 惰性删除 + 定期删除
定期删除+惰性删除结合
一个key 设置过期时间后(expire k seconds),能自动的删除
一个操作将在未来的某个确定时间发生。
一个操作将在未来的某个确定时间发生。
出现的问题
会有过期了的key一直存在,不保证一定能删除缓存,导致内存越来越高
应用场景
订单的过期取消 (下了一个订单,超过30min没有支付,则自动取消)
key的过期删除(给key设置了一个过期时间,到达这个过期时间后,key能自动的删除)
实践:过期时间随机化
学习链接
详解博客文档:
https://blog.csdn.net/alex_xfboy/article/details/88959647
https://blog.csdn.net/alex_xfboy/article/details/88959647
官方文档:http://redis.cn/commands/expire.html
八种内存淘汰策略、数据/内存淘汰机制、内存溢出控制策略
配置
maxmemory-policy
redis的过期策略是什么?
手写个LRU算法?
手写个LRU算法?
缓存是基于内存的,内存是有限的,如果存储数据超过内存大小,数据会被干掉。
设置了key过期时间,但是没到时间就失效了。或者数据过期时间到了,差不到了,但是还占用着内存。
如下因为删除是定期删除+惰性删除
1.假设你设置了一个一批key只能存活一个小时,那么接下来的一个小时后,
redis是怎么对这批key进行删除的?
redis是怎么对这批key进行删除的?
定期删除+惰性删除
定期删除:redis每隔100毫秒就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期了就删除。
注意是随机检查一些key,不是所有的key都遍历一遍检查,否则cpu受不了。
注意是随机检查一些key,不是所有的key都遍历一遍检查,否则cpu受不了。
惰性删除:后面获取某个key的时候,redis会检查一下key是否过期,过期了就删除,不会给你返回任何东西。
并不是key到期了就被删除,而是查询key的时候,懒惰的删除。
并不是key到期了就被删除,而是查询key的时候,懒惰的删除。
2.因为过期key但是没有走惰性删除或者其他的key过多,导致内存块耗尽了,咋整?
走内存淘汰机制
内存淘汰:如果redis占用过多的时候,此时会进行内存淘汰,有如下一些策略
1.noevication:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用,太恶心了。
2.allkeys-lru:当内存满了,在键空间中,移除最近最少使用的key(最少使用的key)
3.allkeys-random:内存满了,随机找一些key删除。
4.volatile-lru:内存满了,只对设置了过期时间的key,使用最少的删除。
5.volatile-random:内存满了,随机删除设置了过期时间的key。
6.volatile-ttl:当内存满了,对有更早过期时间的key优先删除。
1.noevication:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用,太恶心了。
2.allkeys-lru:当内存满了,在键空间中,移除最近最少使用的key(最少使用的key)
3.allkeys-random:内存满了,随机找一些key删除。
4.volatile-lru:内存满了,只对设置了过期时间的key,使用最少的删除。
5.volatile-random:内存满了,随机删除设置了过期时间的key。
6.volatile-ttl:当内存满了,对有更早过期时间的key优先删除。
手写个LRU
内存淘汰策略介绍
数据淘汰策略
Redis设置maxmemory-policy的方式 ,超过最大的容量后会怎么做
描述:Redis是基于内存结构进行数据缓存的,当内存资源消耗完毕,想要有新的数据缓存进来,要从现有的Redis内存结构中释放一些数据
内存淘汰指的是,当配置了内存限制时,redis中存储数据量达到限制值,就要进行数据淘汰。
redis使用的是近似LRU算法
LRU:使用lru算法;random:随机
+ 如果你只是拿 Redis 做缓存,那最好使用 allkeys-xxx,客户端写缓存时不必携带过期时间。
+ 如果你还想同时具备持久化功能,那就使用 volatile-xxx 策略,好处就是,没有设置过期时间的 key不会被LRU算法淘汰
redis使用的是近似LRU算法
LRU:使用lru算法;random:随机
+ 如果你只是拿 Redis 做缓存,那最好使用 allkeys-xxx,客户端写缓存时不必携带过期时间。
+ 如果你还想同时具备持久化功能,那就使用 volatile-xxx 策略,好处就是,没有设置过期时间的 key不会被LRU算法淘汰
ttl和random比较容易理解,实现也会比较简单。主要是Lru最近最少使用淘汰策略,设计上会对key 按失效时间排序,然后取最先失效的key进行淘汰
Redis淘汰策略
描述:Redis是基于内存结构进行数据缓存的,当内存资源消耗完毕,想要有新的数据缓存进来,要从现有的Redis内存结构中释放一些数据
LRU(least recently use)算法
最近没有使用算法
不需要额外的存储空间,使用一个双向连表就能实现
如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。
因此,当空间满时,最久没有访问的数据最先被置换(淘汰)
因此,当空间满时,最久没有访问的数据最先被置换(淘汰)
LFU(least recently use)算法
最近使用频率最少的key
需要一个能记录key 使用次数的空间,使用ZSet 这样的结构就能实现
如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能
性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰
性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰
LRU淘汰原理
最少使用
LRU
LFU淘汰原理
全局的键空间选择性移除
no-enviction(Redis默认,直接抛出异常)
(驱逐)
默认策略,拒绝写入操作
noeviction(Redis默认)
这是默认的淘汰策略!!!
noeviction(默认)
【这种一般没有人用】
noeviction 报错
禁止驱逐数据
不做删除操作,新写入操作报错
当超过最大容量,不会删除任何key,返回一个错误
当内存不足以容纳新数据,新写入数据报错
# noeviction -> 不淘汰任何数据,当内存不够时直接抛出异常.
不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。
这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。
allkeys-lru(最常用)
【最常用】
allkeys-lru
allkeys-lru
allkeys-lru
allkeys-LRU
# allkeys-lru ->
在所有key中LRU
LRU算法删除所有key
通过lru算法,删除最近没有使用的key
从所有数据中淘汰最久未使⽤的数据.
LRU算法
移除最近最少使用的key
从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
当内存不足以容纳新的数据,移除最近最少使用的key
区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。
这意味着没有设置过期时间的 key 也会被淘汰。
使用场景、使用策略规则
数据呈现幂律分布
部分数据访问频率高,部分数据访问频率低
条件
所有key(不和volatile一样)
使用LRU算法
allkeys-random(随机溢出某个key)
内存不足。随机溢出某个key
随机删除所有key
在所有key的集合中随机删除一个key
# allkeys-random -> 从所有数据中随机淘汰⼀批数据.
从数据集(server.db[i].dict)中任意选择数据淘汰
跟上面一样,不过淘汰的策略是随机的 key
allkeys-random
allkeys-random 随机移除某个key
allkeys-random使用场景、使用策略规则
数据呈现平等分布
所有的数据访问频率都相同
设置过期时间的键空间选择性移除
volatile-lru
volatile-LRU
volatile-lru 移除最近最少使用的key
LRU算法删除 有expire的key
volatile-lru LRU算法
LRU: Least Recently Used
Least Recently used 在设置过期时间的key中,删除最近最少使用
当内存不足,在设置了过期时间的key中移除,最近最少使用的key
# volatile-lru -> 从设置了过期时间的数据中淘汰最久未使⽤的数据.
从设置了过期时间的key集合中删除最近没有使用的key
从设置了过期时间的数据中淘汰最久未使⽤的数据.
从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
两个条件:
+ 设置了过期时间
+ 使用了LRU算法淘汰最少使用
volatile-random
内存不足,在设置了过期时间的key中随机移除一个key
volatile-random 随机移除某个key
随机删除过期key
在设置了过期时间的key的集合中随机删除一个key
从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
# volatile-random -> 从设置了过期时间的数据中随机淘汰⼀批数据.
volatile-random
跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key
+ 设置了过期时间
+ 随机淘汰
volatile-ttl
volatile-TTL
volatile-ttl
删除最近将要过期key
Time To Live 在设置过期时间的key中,删除剩余存活时间最短的key
volatile-ttl 有更早过期时间的key优先移除
在设置了过期时间的key集合中删除即将过期的key
内存不足,在设置过期时间的key中,有更早过期时间的key被移除
# volatile-ttl -> 淘汰过期时间最短的数据.
从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。
根据剩余过期时间来淘汰
其他内存淘汰策略
volatile-lfu
LFU: Least Frequently Used
volatile-lfu
# volatile-lfu -> 从设置了过期时间的数据中淘汰使⽤最少的数据.
从设置了过期时间的key集合中删除最近使用频率最少的key
从设置了过期时间的数据中淘汰使⽤最少的数据.
allkeys-lfu
allkeys-lfu
# allkeys-lfu -> 从所有数据中淘汰使⽤最少的数据.
在所有key的集合中 删除最近使用频率最少的key
allkeys-TTL
在所有key中TTL
冷热数据分离
热数据经常访问,放在内存中,冷数据写入磁盘
用VM实现
LRU算法
基于linkedhashmap实现最简单的lru【核心是他的removeEldestEntry】
序列化与压缩
拒绝Java原生
推荐protobuf, kryo, snappy
内存的好处
读写快速
内存断电即失
持久化
内存查看:info memory
used_memory
redis自身内存
对象内存
优化
key: 不要过长
value: ziplist / intset 等优化方式
内存碎片
缓冲内存
客户端缓冲区
输出缓冲区
普通客户端
normal 0 0 0
默认无限制,注意防止大命令或 monitor:可能导致内存占用超大!!
找到monitor客户端:client list | grep -v "omem=0"
slave 客户端
slave 256mb 64mb 60
可能阻塞:主从延迟高时,从节点过多时
pubsub 客户端
pubsub 32mb 8mb 60
可能阻塞:生产大于消费时
输入缓冲区
最大 1GB
复制缓冲区
repl_back_buffer
默认1M,建议调大 例如100M
防止网络抖动时出现slave全量复制
AOF 缓冲区
无限制
lua内存
used_memory_rss
从操作系统角度看redis进程占用的总物理内存
mem_fragmentation_ratio
内存碎片 used_memory_rss / used_memory > 1
内存碎片必然存在
优化
避免频繁更新操作:append, setrange
安全重启
mem_allocator
子进程内存消耗
场景
bgsave
bgrewriteaof
优化
去掉 THP 特性
观察写入量
overcommit_memory = 1
两种持久化操作/机制/方式
两种Redis的持久化机制
RDB快照(默认开启)
简介
1、在指定时间间隔内将内存中的数据集快照写入磁盘,恢复时将快照文件直接读到内存中;
2、默认开启
默认方式,不需要进行配置,默认就使用这种机制
* 在一定的间隔时间中,检测key的变化情况,然后持久化数据
* 在一定的间隔时间中,检测key的变化情况,然后持久化数据
3、Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束
了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高
的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加
的高效。
了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高
的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加
的高效。
4、最后一次持久化的数据可能丢失
保存策略
save second count:second秒内如果至少有count个key值变化,则进行保存
save "":禁用RDB模式
属性配置
save:保存策略
dbfilename:RDB快照文件名
dir:指定RDB快照保存的目录
stop-writes-on-bgsave-error:备份出错时,是否继续接受写操作,默认会停止
rdbcompression:是否压缩存储快照,若压缩则会采用LZF算法进行压缩
rdbchecksum:是否进行数据校验,若校验则会采用CRC64算法来校验,会增加10%的性能消耗
特点
5分钟一次
冷备
恢复的时候比较快
快照文件生成时间久,消耗cpu
优缺点
优点
(1)redis主进程调用fork()方法创建一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
(2)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
缺点:快照持久化期间修改的数据不会被保存,可能丢失数据。
步骤
1. 编辑redis.windwos.conf文件
# after 900 sec (15 min) if at least 1 key changed
save 900 1
# after 300 sec (5 min) if at least 10 keys changed
save 300 10
# after 60 sec if at least 10000 keys changed
save 60 10000
# after 900 sec (15 min) if at least 1 key changed
save 900 1
# after 300 sec (5 min) if at least 10 keys changed
save 300 10
# after 60 sec if at least 10000 keys changed
save 60 10000
2. 重新启动redis服务器,并指定配置文件名称
D:\JavaWeb2018\day23_redis\资料\redis\windows-64\redis-2.8.9>redis-server.exe redis.windows.conf
D:\JavaWeb2018\day23_redis\资料\redis\windows-64\redis-2.8.9>redis-server.exe redis.windows.conf
RDB是什么?
RDB的全称
Redis DataBase
redis database
RDB(Redis DataBase)
RDB的特点
内存快照,将内存中所有数据记录到磁盘
按照一定的时间将内存的数据以快照的形式,保存到硬盘中
在指定时间间隔内,将内存中的数据集快照写入磁盘
在指定的时间间隔对你的数据进行快照存储
能够在指定的时间间隔能对你的数据进行快照存储
快照(snapshotting)
行话说的snapshot快照,恢复时是将快照文件直接读到内存里
RDB默认开启
默认开启
RDB(默认)
RDB(Redis DataBase)文件
RDB(Redis Database)(默认)
rdb(redis database)默认开启
RDB/快照的最佳配置、属性配置、相关配置
save:保存策略、时间策略
保存策略
Save the DB on disk:保存数据库到磁盘
生成dump.rdb 通过配置文件save参数来定义快照周期
save m n 时间策略
#配置快照(rdb)促发规则
格式
save <seconds> <changes>
save <秒> <更新>
save second count
second秒内如果至少有count个key值变化,则进行保存
如果指定的秒数和数据库写操作次数都满足了,就将数据库保存。
下面是保存操作的实例
save 900 1
900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化)
900秒内至少有1个key被改变则做一次快照
表示900s内有1条写入,就产生快照
save 300 10
300秒内至少有300个key被改变则做一次快照
300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化)
表示300s内有10条写入,就产生快照
save 60 10000
60秒内至少有10000个key被改变则做一次快照
60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化)
save ""
save ""
禁用RDB模式
关闭该规则使用
注释:注释掉“save”这一行配置项就可以让保存数据库功能失效。
你也可以通过增加一个只有一个空字符串的配置项(如下面的实例)来去掉前面的“save”配置。
dbfilename dump.rdb
dbfilename dump.rdb
dbfilename dump-${port}.rdb
dbfilename:RDB快照文件名
#rdb持久化存储数据库文件名,默认为dump.rdb
导出数据库的文件名称
图解
dir ./
dir "/etc/redis"
dir /bigdiskpath
dir ./
dir:指定RDB快照保存的目录
工作目录
#数据文件存放目录,rdb快照文件和aof文件都会存放至该目录,请确保有写权限
导出的数据库会被写入这个目录,文件名就是上面'dbfilename'配置项指定的文件名。
只增的文件也会在这个目录创建
注意你一定要在这个配置一个工作目录,而不是文件名称。
stop-writes-on-bgsave-error yes
stop-write-on-bgsave-error yes
备份出错时,是否继续接受写操作,默认会停止
yes代表当使用bgsave命令持久化出错时候停止写RDB快照文件
no表明忽略错误继续写文件
#在默认情况下,如果RDB快照持久化操作被激活(至少一个条件被激活)并且持久化操作失败,Redis则会停止接受更新操作。
#这样会让用户了解到数据没有被正确的存储到磁盘上。否则没人会注意到这个问题,可能会造成灾难。
#
#如果后台存储(持久化)操作进程再次工作,Redis会自动允许更新操作。
#
#然而,如果已经恰当的配置了对Redis服务器的监视和备份,你也许想关掉这项功能。
#如此一来即使后台保存操作出错,redis也仍然可以继续像平常一样工作。
#这样会让用户了解到数据没有被正确的存储到磁盘上。否则没人会注意到这个问题,可能会造成灾难。
#
#如果后台存储(持久化)操作进程再次工作,Redis会自动允许更新操作。
#
#然而,如果已经恰当的配置了对Redis服务器的监视和备份,你也许想关掉这项功能。
#如此一来即使后台保存操作出错,redis也仍然可以继续像平常一样工作。
rdbcompression yes
rdbcompression yes
rdbcompression:是否压缩存储快照,若压缩则会采用LZF算法进行压缩
#是否开启RDB文件压缩,该功能可以节约磁盘空间、
#是否在导出.rdb数据库文件的时候采用LZF压缩字符串和对象?
#默认情况下总是设置成‘yes’, 他看起来是一把双刃剑。
#如果你想在存储的子进程中节省一些CPU就设置成'no',
#但是这样如果你的kye/value是可压缩的,你的到处数据接就会很大。
#默认情况下总是设置成‘yes’, 他看起来是一把双刃剑。
#如果你想在存储的子进程中节省一些CPU就设置成'no',
#但是这样如果你的kye/value是可压缩的,你的到处数据接就会很大。
rdbchecksum yes
rdbchecksum yes
rdbchecksum:是否进行数据校验
若校验,则会采用CRC64算法来校验,会增加10%的性能消耗
#在写入文件和读取文件时是否开启rdb文件检查,检查是否有无损坏,如果在启动是检查发现损坏,则停止启动。
#从版本RDB版本5开始,一个CRC64的校验就被放在了文件末尾。
#这会让格式更加耐攻击,但是当存储或者加载rbd文件的时候会有一个10%左右的性能下降,
#所以,为了达到性能的最大化,你可以关掉这个配置项。
#这会让格式更加耐攻击,但是当存储或者加载rbd文件的时候会有一个10%左右的性能下降,
#所以,为了达到性能的最大化,你可以关掉这个配置项。
没有校验的RDB文件会有一个0校验位,来告诉加载代码跳过校验检查
RDB的原理、具体原理
图解RDB的原理
图解RDB的原理
时点性
当条件满足,redis需要执行RDB的时候,服务器会执行以下操作
步骤与注意事项
步骤1:创建(fork)子进程来进行持久化,将数据写入到一个临时文件中
创建(fork)子进程生成快照
操作redis时,创建fork子进程进行持久化
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中
将内存内容以快照的形式写入临时RDB文件中
rdb保存的文件是dump.rdb
Redis会单独创建(fork)一个与当前进程一模一样的子进程来进行持久化,将数据写入到一个临时文件中
子进程将数据集写入到一个临时 RDB 文件中。
每隔几分钟,几小时,几天,生成redis内存中的数据的一份完整的快照
每隔几分钟,几小时,几天,生成redis内存中的数据的一份完整的快照
fork一个子进程,从父进程中同步复制数据,保存快照
使用fork指令,创建子进程,并且和父进程一样指向对应的数据(物理地址)
redis调用系统函数fork() ,创建一个子进程。
fork是系统调用,RDB时,会调用一次fork
Redis fork子进程bgsave,共享主线程内存数据
内存越大,耗时越长
info: latest_fork_usec
步骤2:当父进程发生修改时,Copy On Write复制一份数据并发生修改
当父进程发生修改时,copyonwrite(写时复制)复制一份数据并发生修改;
Redis使用操作系统的COW( Copy On Write)机制来实现快照持久化
copy on write机制
COW( Copy On Write)机制
借助操作系统提供的写时复制(COW),在执行快照的同时,正常处理写操作
在执行fork时,操作系统(类Unix操作系统)会使用写时复制(copy-on-write)策略
即fork函数发生的一刻,父子进程共享同一内存数据
当父进程要更改其中某片数据时(如执行一个写命令 ),操作系统会将该片数据复制一份,以保证子进程的数据不受影响
新的RDB文件存储的是执行fork那一刻的内存数据。
步骤3:临时文件替换上次持久化好的dump.rdb文件
待持久化结束后,替换上次持久化好的文件
快照写入完成后,替换原来的快照rdb文件
待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
持久化结束后,将快照保存成dump.rdb文件,释放内存
当子进程完成对临时RDB文件的写入时,redis 用新的临时RDB 文件替换原来的RDB 文件,并删除旧 RDB 文件。
Redis在进行快照的过程中,不会修改RDB文件,只有快照结束后,才会将旧的文件替换成新的
任何时候RDB文件都是完整的。可以通过定时备份RDB文件来实 现Redis数据库备份。
步骤4:子进程退出,整个过程中,主进程是不进行任何 IO 操作的
整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能
同时,子进程是不会感知到变化,这样就保证了当RDB的瞬间,子进程中的所有数据是当前数据,不会受到父进程的影响。
相当于两个redis进程,这期间主线程不参与持久化,保证redis的高性能
步骤5:恢复时将快照文件直接读到内存中,恢复dump.rdb文件
恢复时,将快照文件直接读到内存中
如何恢复dump.rdb文件 ?
图解如何恢复rdb文件?
1、只需要将rdb文件放到redis启动目录就可以,redis启动的时候会自动检查dump.rdb恢复数据
把dump.rbd放在启动目录就可以了,redis启动时会自动检查
2、查看需要存在的位置
config get dir 就能直到对应的redis启动目录
最后一次持久化的数据可能丢失
多长时间内至多操作多少次就会保存,可能会损失最后一次的数据
RDB的缺点就是最后一次持久化的数据可能丢失
如果需要大规模数据的恢复,且对数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。
通过RDB方式实现持久化,一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。
RDB的dump.rdb文件触发规则、触发机制、触发条件
数据初始化
从节点发送命令主节点做bgsave,同时开启buffer
rdb的dump.rdb文件触发规则
备份就会自动生成一个dump.rdb文件
触发机制不容忽略方法
全量复制
debug reload
手动触发
SAVE
save
save规则
同步
由主进程进行快照操作
阻塞客户端命令
会阻塞住其他请求
不消耗额外内存
save的规则满足的情况下,会自动触发rdb规则
save命令会阻塞主线程,一般不用
SAVE是同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘中。
需要主动调用save命令
适用于在主动关机情况,操作redis去RDB
BGSAVE
bgsave
BGSAVE
异步
不阻塞客户端命令
从节点SYNC时 (=BGSAVE)
bgsava会fork子进程异步持久化
fork子进程,消耗内存
通过fork子进程进行快照操作。
BGSAVE是异步操作,会fork出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成;
Redis 服务器在BGSAVE 执行期间仍然可以继续处理客户端的请求
调用场景
redis配置文件conf配置:设置多久,或者达到多少量去进行RDB
主动调用
自动触发、自动保存
配置文件: save seconds changes
配置文件
不建议打开
配置文件中有快照配置
例如:save 900 1 (15分钟内有一次修改)
shutdown
退出redis,也会产生rdb文件
客户端执行SHUTDOWN、shutdown命令时
flushall
执行flushall命令,也会触发rdb规则
执行flushdb,也会产生dump.rdb,但为空文件
RDB的优点、优势
适用于对数据的完整性要求不高的场景
对数据的完整性要求不高
因为rdb这边有一个配置save 900 10类型的配置,有一个最低要求,如果再这期间宕机,就不会生成文件,导致文件丢失
适合大规模数据恢复,恢复数据比AOF快
适合大规模的数据恢复
恢复大的数据集时,RDB方式会更快一些
恢复时比较快,适合大规模的数据恢复
相对于数据集大时,比AOF的启动效率更高
RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
父进程无须执行任何磁盘的IO操作,因此恢复数据比AOF快
生成RDB文件时,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
相对于AOF持久化机制来说,基于RDB数据文件来重启和恢复redis进程,更加快速。
RDB 在恢复大数据集时的速度,比 AOF 的恢复速度要快
同样的数据文件比AOF文件小,数据恢复时比AOF要快
RDB是一个非常紧凑(compact)的文件,它保存了redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。
RDB文件是经过压缩的二进制格式,所以占用的空间会小于内存中的数据大小,更加利于传输。
(可以配置rdbcompression参数以禁用压缩节省CPU占用)
最大化Redis的性能,子线程处理IO持久化
性能最大化,子线程处理IO持久化
子进程负责所有的RDB操作,父进程不影响IO,最大化redis的性能。
redis主进程调用fork()方法,创建一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
RDB对Redis的性能影响非常小,是因为在同步数据的时候fork了一个子进程去进行持久化
RDB对redis提供对外读写服务,影响非常小,可以让redis保持高性能,
因为redis只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。
RDB:redis写数据,一般只需要写入内存,只有特定时间,才会写入磁盘
AOF:每次写数据都要写入AOF,虽然有os cache,但是还是有影响
AOF:每次写数据都要写入AOF,虽然有os cache,但是还是有影响
非常适合做冷备
每个数据文件都代表了某一时刻中redis的数据,这种多数据文件的方式
可以通过shell脚本将备份文件发送到一些远程的安全存储上去,以预定好的备份策略来定期备份redis中的数据。
容灾性好,一个文件可以保存到安全的磁盘
只有一个文件dump.rdb,方便持久化
因为可以设置每个时间段去rdb,所以可以恢复不同时间段的所有全量数据。
RDB的缺点、劣势
耗时 O(n)、耗性能、耗内存
fork子进程占用一定空间
需要一定的时间间隔操作
需要一定的时间间隔进程操作
fork大量数据时,有可能会影响性能
如果生成的快照文件比较大会影响redis的性能
在数据集比较庞大时, fork() 可能会非常耗时
每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。
在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端;
如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。
虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。
RDB在生成数据文件时,如果文件很大,客户端会暂停几毫秒甚至几秒
创建fork子进程时,也会占用一定的内容空间(只要是进程都会占用空间)
RDB每次在fork子进程来执行RDB快照数据文件生成时,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒甚至数秒。
一定不要让RDB间隔时间太长,否则每次生成RDB文件太大,对redis本身性能可能会有影响
Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存。
根据数据量大小与结构和服务器性能不同,这个时间也不同。
通常将一个记录一千万个字符串类型键、大小为1GB的快照文件载入到内 存中需要花费20~30秒钟。
数据丢失(最大的缺点、不适合做第一优先恢复方案)
不可控、会丢失数据
会丢失fork后一段时间内的文件
如遇突然宕机,丢失的数据比较多
如果期间redis宕机,最后一次数据无法保存
数据安全低,持久化的间隔期间宕机,导致数据丢失
快照持久化期间修改的数据不会被保存,可能丢失数据。
如果redis意外宕机了,这个最后一次修改的数据就没有了
如果你需要尽量避免在服务器故障时丢失数据,那么RDB 不适合你。
虽然Redis 允许设置不同的保存点(save point)来控制保存 RDB 文件的频率
但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。
因此,可能会至少 5 分钟才保存一次 RDB 文件。
在这种情况下, 一旦发生故障停机, 就可能会丢失好几分钟的数据。
快照文件默认五分钟才生成一次(根据时间策略),这意味着这五分钟的数据都有可能丢失
如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。
一般来说RDB快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候一旦redis宕机,可能丢失这段时间的数据。
RDB文件时特定的格式,阅读性差,格式固定,可能存在不兼容的情况
AOF写日志
简介
AOF 是以日志的形式来记录每个写操作,将每一次对数据进行修改,都把新建、修改数据的命令保存到指
定文件中。
定文件中。
Redis 重新启动时读取这个文件,重新执行新建、修改数据的命令恢复数据。
日志记录的方式,可以记录每一条命令的操作。可以每一次命令操作后,持久化数据
默认关闭
AOF 文件的保存路径,同 RDB 的路径一致。
AOF 在保存命令的时候,只会保存对数据有修改的命令
当 RDB 和 AOF 存的不一致的情况下,按照 AOF 来恢复
如果AOF文件中出现了残余命令,会无法重启服务,需要使用redis-check-aof工具来修复:redis-check-aof -fix 文件
保存策略
appendfsync always:每次执行写操作都保存
appendfsync everysec(默认):每秒保存一次,可能会丢失1秒的数据
appendfsync no:从不保存
特点
appendOnly
数据齐全
回复慢文件大
优缺点
优点
备份机制稳健,丢失数据概率低
可读的日志文本,可以处理误操作
缺点
比RDB占用更多的磁盘空间
恢复备份较慢
步骤
编辑redis.windwos.conf文件
appendonly no(关闭aof) --> appendonly yes (开启aof)
# appendfsync always : 每一次操作都进行持久化
appendfsync everysec : 每隔一秒进行一次持久化
# appendfsync no : 不进行持久化
# appendfsync always : 每一次操作都进行持久化
appendfsync everysec : 每隔一秒进行一次持久化
# appendfsync no : 不进行持久化
属性配置
appendonly:是否开启AOF
appendfilename:AOF备份文件的名称
appendfsync:保存策略
no-appendfsync-on-rewrite:重写时,是否执行保存策略
auto-aof-rewrite-percentage:指定重写与否的aof文件大小比例
auto-aof-rewrite-min-size:设置允许重写的最小aof文件大小
aof-load-truncated:是否截断
AOF是什么?
AOF
AOF 机制
AOF(日志)
AOF(Append Only File)日志
AOF(Append Only File)
append only module
APPEND ONLY MODE
AOF(Append-only file)
Append-only file,AOF
AOF特点
只追加操作的文件
以日志的形式来记录每个写操作
以追加操作日志的方式,将数据持久化到文件中
将在redis操作的所有命令全部记录下来,以日志的形式,恢复时,将这些文件全部执行一遍
诞生背景
快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。
从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化
默认关闭
默认关闭
默认是不开启AOF模式的,需要手动进行配置修改
开启方法
需手动开启:appendonly yes
redis.conf中把appendonly改成yes
打开AOF配置,在配置文件中打开AOF方式
appendonly yes
appendonly yes
appendonly yes
线上开启方式
config set appendonly yes
开启后redis会保留一块内存供缓存使用,默认是1M
aof和rdb同时开启时,只保留save 900 1 减少fork子进程的次数(优化点)
AOF的原理
图解AOF工作原理
图解AOF工作原理
图解AOF工作原理
AOF具体步骤
步骤1:将所有的写命令,追加到AOF缓冲区中
以日志的形式记录每个写操作
将所有的写命令追加到AOF缓冲区中
将redis执行过的所有指令记录下来(读操作不记录)
AOF 在保存命令时,只会保存对数据有修改的命令
只许追加文件,但不可以改写文件
记录每一次的写操作
写命令刷新到缓冲区
每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。
将每一次对数据进行修改,都把新建、修改数据的命令保存到指定文件中。
步骤2:根据写入策略,将每条命令 fsync 到硬盘AOF文件
由主线程完成
AOF日志是在主线程中执行,日志文件写回磁盘存在风险
AOF 文件的保存路径,同 RDB 的路径一致。
根据对应的写入策略向硬盘进行同步操作
每来一条数据都会写入到AOF,但是实际上是写到操作系统os cache中,
所以redis每隔1s会调用一次操作系统fsync操作,强制将os cache中的数据刷入到磁盘文件中。
执行命令后记录,记录redis收到的每一条命令
AOF追加阻塞
对比上次fsync时间,>2s则阻塞
info: aof_delayed_fsync (累计值)
fsync介绍
是什么?
fsync(int id)函数
谁提供
Linux的glibc提供
作用
将指定文件的内容强制从内核缓存刷到磁盘
步骤3:读取和执行存放的指令日志,进行AOF的故障恢复
redis启动之初会读取该文件重新构建数据
恢复数据,重启redis就可以生效了,不需要固定的位置保存
当 Redis 重新启动时, 程序就可以通过重新执行 AOF 文件中的命令,来达到重建数据集的目的。
当Redis重启后,就根据日志文件的内容将指令从头开始执行一遍,以完成数据恢复的工作
重启Redis,会重新将持久化的日志文件恢复数据
数据恢复时,重新运行这些命令,重构数据
Redis 重新启动时,读取这个文件,重新执行新建、修改数据的命令恢复数据。
AOF存放的是指令日志,恢复需要读取和执行存放的指令
当 RDB 和 AOF 存的不一致的情况下,按照 AOF 来恢复
将AOF日志中所有命令执行一遍
AOF的触发机制
appendfsync:保存与同步策略
bgrewriteaof:AOF重写机制
AOF的重写机制
AOF重写机制是什么?
rewrite
重写操作
AOF重写机制
重写机制(bgrewriteaof)
为什么需要重写?重写作用
AOF文件只有一个,会越来越大
redis中的数据是有一定限量的,但是AOF是存放每条写命令的,所以会不断的膨胀
当AOF大到一定程度会进行rewrite操作。
随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
基于当时redis内存中的数据,来重新构造一个更小的AOF,然后删除原来膨胀很大的AOF
1、减少磁盘占用量
2、加速恢复速度
重写后的文件为什么会变小 ?
进程内已超时的数据不再写入文件,而且多条写命令可以合并为一条
新的AOF文件只保留最终数据的写入命令(去掉了修改命令)
修改配置文件
推荐配置
auto-rewrite-min-size 64M
auto-aof-rewrite-min-size
设置允许重写的最小aof文件大小
当前AOF文件启动新的日志重写过程的最小值
避免刚刚启动Reids时,由于文件尺寸较小导致频繁的重写
auto-aof-rewrite-min-size 64mb
aof文件大于64M时重写
生产环境下要配置的比较大
由于重写会fork子进程,为了减少重写次数,建议配置5GB以上(优化点)
当前AOF文件的大小是上次rewrite后的两倍且大于64M
auto-aof-rewirte-percentage 100
auto-aof-rerwrite-percentage
指定重写与否的aof文件大小比例
当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
增长的百分比
指超过优化后,第二次优化文件大小大于第一次优化文件后大小一倍时开始重写
比如aof文件累积到64M开始第一次重写,重写后的大小为40M,
如果auto-aof-rewrite-percentage参数值是100,则当aof文件超过80M时才发生第二次重写
动态应用配置
config set appendonly yes
config rewrite
使用命令bgwriteaof
也可使用命令bgwriteaof
Redis fork子进程bgrewriteaof,根据Redis现状创建一个新的AOF文件,每个键值对用一条命令记录它的写入,以缩小日志文件
bgrewriteaof 命令
fork子进程来进行
fork一个子进程,保存写操作
本质上,AOF也同样使用了fork操作
类似bgsave, fork子进程,重新生成AOF
bgrewriteaof
BGREWRITEAOF命令
执行效果
执行BGREWRITEAOF命令时
Redis服务器会维护一个AOF重写缓存区
该缓冲区会在子进程创建新AOF期间记录所有写命令,子进程完成创建AOF后,服务器将缓冲区的所有内容追加到新AOF末尾
根据Redis版本不同有些区别
4.0版本以前
追加完AOF后,发生重写,删除抵消的指令,合并重复的执行
(如incr 1万次,最后执行结果为incrby 10000)
4.0版本之后
发生重写时,会将老的数据RDB到AOF文件中,将增量的数据以指令的方式append到AOF中
redis2.4之后
就可以配置自动触发AOF 重写
AOF文件的修复方式
AOF保存的文件是appendonly.aof
Redis执行的每次命令记录到单独的日志appendonly.aof文件
如果AOF文件中出现了残余命令,会无法重启服务
如果aof文件出现错误,这时候redis时启动不起来的,就需要修复aof文件,
redis提供一个工具:redis-check-aof --fix appendonly.aof
使用redis-check-aof工具来修复:redis-check-aof -fix 文件
AOF的配置文件、属性配置、配置项、相关配置
appendonly
是否开启AOF
是否开启AOF,默认关闭(no)
appendonly yes
appendfilename
AOF备份文件的名称
指定 AOF 文件名
appendfilename appendonly.aof
appendfsync:保存与同步策略
是什么?
模式
触发条件
保存策略
保存操作记录的模式
设置同步策略
三种不同的刷写模式
写入策略
写操作AOF会触发IO,降低性能
调整AOF刷新策略(模式可配置)(将数据flush到磁盘中)
Always(安全,低效)
alway
appendfsync always
每次执行写操作都保存
把每个写命令都立即同步到aof文件中,
always: 每次有数据修改时添加记录 性能差
同步写回
每执行一条指令,将其刷新到磁盘中
每次收到写命令就立即强制写入磁盘,是最有保证的完全的持久化,
但速度也是最慢的,一般不推荐使用。
很慢,但很安全
always:每次发生数据变更时立即同步到磁盘,效率低,安全
追求安全
每次执行都记录
Everysec(推荐、默认配置、兼顾速度和安全)
everysec
every second
everysec(每秒)
appendfsync everysec
【默认也推荐使用】
(推荐)
(默认)
AOF根据模式同步数据,一般是everysec
默认开启的策略
受推荐的方式
每秒同步一次
每秒保存/记录/刷新一次
每秒开启一次
每秒同步一次
每秒写回
每秒钟强制写入磁盘一次
每隔一秒左右执行一次fsync操作
每隔一秒执行一次fsync操作
同步方式:1s
每秒把缓冲区的数据写入磁盘
可能会丢失1秒的数据
最多丢失1s的数据
最多损失一秒的数据
可能会丢掉1s的数据
可能会丢失1秒的数据
可能会丢失一秒的数据
最多损失一秒的数据
其他特点
效率高
每次操作,不同步
丢失数据介于always与no中间
可以兼顾速度和安全
在性能和持久化方面做了很好的折中
No(不推荐,高效,不安全)
no
appendfsync no
操作系统控制写回
由OS决定何何时同步,
linux通常是30s
一般为30秒左右一次
从不保存
不处理,非常快,但也最不安全
不主动去刷新,只有缓存行满了,内核自己刷到磁盘中
完全依赖OS的写入
性能最好但是持久化最没有保证,不被推荐。
等到缓冲区满了才写入磁盘,次数少,效率高,不安全
追求效率
auto-aof-rewrite-min-size
设置允许重写的最小aof文件大小
当前AOF文件启动新的日志重写过程的最小值,避免刚刚启动Reids时,由于文件尺寸较小导致频繁的重写。
auto-rewrite-min-size 64M
auto-aof-rewrite-min-size
设置允许重写的最小aof文件大小
当前AOF文件启动新的日志重写过程的最小值
避免刚刚启动Reids时,由于文件尺寸较小导致频繁的重写
auto-aof-rewrite-min-size 64mb
aof文件大于64M时重写
生产环境下要配置的比较大
由于重写会fork子进程,为了减少重写次数,建议配置5GB以上(优化点)
当前AOF文件的大小是上次rewrite后的两倍且大于64M
auto-aof-rewrite-percentage
指定重写与否的aof文件大小比例
当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。
auto-aof-rewirte-percentage 100
auto-aof-rerwrite-percentage
指定重写与否的aof文件大小比例
当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
增长的百分比
指超过优化后,第二次优化文件大小大于第一次优化文件后大小一倍时开始重写
比如aof文件累积到64M开始第一次重写,重写后的大小为40M,
如果auto-aof-rewrite-percentage参数值是100,则当aof文件超过80M时才发生第二次重写
no-appendfsync-on-rewrite
重写时,是否执行保存策略
#设置为yes,表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no
#在日志重写时,不进行命令追加操作,而只是将其放在缓冲区里,避免与命令的追加造成DISK IO上的冲突。
no-appendfsync-on-rewrite no
aof-load-truncated
是否截断
其他的AOF配置可以保持默认的,默认aof命令是无限追加的
AOF的优势、优点
丢失数据少
相比于RDB,丢失的数据少
数据安全
appendfsync always 每次执行都记录
每一次修改都同步,文件的完整性更好
备份机制稳健,丢失数据概率低
每隔一秒左右(模式可配置)执行一次fsync操作,最多损失一秒的数据
丢失数据介于always与no中间,在性能和持久化方面做了很好的折中
可以更好保护数据不丢失,每隔1s就执行一次fsync操作,保证os cache数据写入磁盘中,redis挂了,最多丢失1s数据
写入性能高,减少了磁盘寻址的开销
rewrite操作对性能影响很小
从不同步,效率最高的!
记录日志是追加(append_only)的方式,减少了磁盘寻址的开销
写入性能高,而且文件不容易破损
文件尾部破损,也很容易恢复
宕机,可通过redis-check-aof工具解决一致性问题
AOF开启后,支持的写QPS会比RDB支持的写QPS更低,因为AOF一般会配置成每秒fsync一次日志文件。
当然,每秒一次fsync,性能还是很高的。
如果你要保证一条数据都不丢失,也是可以的,设置成每写一条数据,进行一次fsync,会导致redis的QPS性能大降。
可读的日志文本,可以处理误操作
AOF日志文件的命令通过非常可读的方式进行记录
这个特性非常适合做灾难性的误删除的紧急恢复
比如
某人不小心用了flush all清空了所有数据,
aof没被rewrite之前,可以删除一些命令
只要这个时候后台rewrite还没有发生,那么立刻拷贝AOF文件,
把最后一条flush all命令删了,再放回去,重启Redis自动恢复所有数据。
建议与RDB同时开启
AOF的劣势、缺点
恢复备份较慢,运行效率慢
恢复慢
恢复速度慢
修复的速度比rdb慢
做数据恢复时,会比较慢
由于需要一条一条执行操作日志中的指令
AOF运行效率也比RDB慢,因此redis默认的配置是RDB持久化
占用更多的磁盘空间
比RDB占用更多的磁盘空间
数据快照文件更大
体量无限变大
相对于数据文件来说,AOF远远大于RDB
对于同一份文件来说,AOF日志文件通常比RDB数据快照文件更大
不合适做冷备(比较大的缺点)
做冷备,定期的备份,不太方便
可能要自己手写复杂的脚本去做,做冷备不合适
恢复不稳定,容易出bug
RDB和AOF的区别,两种方案对比
RDB的体积比AOF小
AOF的体积更大
RDB恢复数据的速度比AOF快
RDB性能比AOF好
RDB数据安全差,很容易丢失数据,AOF根据策略决定
AOF比RDB更安全
AOF文件比RDB更新频率高,优先使用AOF还原数据
如何选择合适的持久化方式?到底该如何选择
选择总结
最好两种策略配合使用
可以单独使用RDB,不建议单独使用AOF
若只是将Redis作为纯内存缓存,可以都不使用
如何选择RDB和AOF,各自的使用场景
可以单独使用RDB,但不要仅仅使用RDB
可以单独使用RDB
可以接受分钟内的数据丢失,选择RDB
因为那样会导致你丢失很多数据
不推荐只使用AOF
不建议单独使用AOF
也不要仅仅使用AOF
如果数据很重要,以至于无法承受任何损失,则可以考虑使用AOF方式进行持久化。
因为它会有两个问题
通过AOF做冷备,没有RDB做冷备恢复的速度快。
因为RDB可以定时对数据库备份,并且恢复数据集的速度快,也可以避免AOF的bug
RDB每次简单粗暴生成快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug
这就需要开发者根据具体的应用场合,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受的范围。
注意:一般在工作中,生产环境使用redis时,会将redis的rdb文件进行备份,以防止数据丢失
RDB和AOF的最佳策略
RDB集中管理
RDB在主机中关掉,在从机中开启
AOF开启缓存和存储
AOF重写集中管理
AOF的策略是每秒保存
RDB和AOF混合使用,同时开启两种持久化机制
混合模式
redis4.0开始支持该模式
为了解决的问题
redis在重启时通常是加载AOF文件,但加载速度慢因为RDB数据不完整,所以加载AOF
开启方式
aof-use-rdb-preamble true开启后,AOF在重写时会直接读取RDB中的内容
运行过程
通过bgrwriteaof完成,不同的是当开启混合持久化后
1 子进程会把内存中的数据以RDB的方式写入aof中,
2 把重写缓冲区中的增量命令以AOF方式写入到文件
3 将含有RDB个数和AOF格数的AOF数据覆盖旧的AOF文件新的AOF文件中,一部分数据来自RDB文件,一部分来自Redis运行过程时的增量数据
2 把重写缓冲区中的增量命令以AOF方式写入到文件
3 将含有RDB个数和AOF格数的AOF数据覆盖旧的AOF文件新的AOF文件中,一部分数据来自RDB文件,一部分来自Redis运行过程时的增量数据
数据恢复
当我们开启了混合持久化时,启动redis依然优先加载aof文件,aof文件加载可能有两种情况如下:
aof文件开头是rdb的格式, 先加载 rdb内容再加载剩余的 aof。
aof文件开头不是rdb的格式,直接以aof格式加载整个文件。
当我们开启了混合持久化时,启动redis依然优先加载aof文件,aof文件加载可能有两种情况如下:
aof文件开头是rdb的格式, 先加载 rdb内容再加载剩余的 aof。
aof文件开头不是rdb的格式,直接以aof格式加载整个文件。
优点:既能快速备份又能避免大量数据丢失
缺点:RDB是压缩格式,AOF在读取它时可读性教差
优先读取与加载AOF文件
会优先使用AOF
RDB启动优先级比AOF低
如果2个都配置,优先加载AOF
当AOF和RDB同时开启,优先读取AOF文件(数据安全性)
同时使用RDB和AOF,重启优先加载AOF来恢复原始数据(数据全)
RDB数据会写入AOF文件头部
redis先加载AOF文件来恢复原始数据,因为AOF数据比rdb更完整,但是aof存在潜在的bug
如把错误的操作记录写入了AOF,会导出数据恢复失败所以可以把RDB作为后备数据为了考虑性能,可以只在Slave上开启RDB,并且15min备份一次
如果为了避免AOF rewite的IO以及阻塞,可以在Redis集群中不开启AOF,靠集群的备份机制来保证可用性,
在启动时,选取较新的RDB文件如果集群全部崩溃,会丢失15min前的数据
Redis启动后,持久化文件的加载流程
先判断是否开启了AOF,如果存在AOF文件,则直接加载AOF文件
如果找不到AOF文件,则直接启动,不会加载RDB文件
如果没有开启AOF,则会加载RDB文件
生产环境建议AOF和RDB同时使用,RDB做灾难备份
混合使用的缺点
同时开启,性能开销大,内存消耗更快,会同时生产rdb文件和aof文件
综合AOF和RDB,最好两种策略配合使用
用AOF来保证数据不丢失,作为数据恢复的第一选择
用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用时,还可以使用RDB进行快速的数据恢复。
持久化操作主要在从节点进行
若只是将Redis作为纯内存缓存,可以都不使用
图解
图解
学习地址
单机持久化
http://redis.cn/topics/persistence.html
Redis持久化运维常见问题
1、fork操作
1、同步操作
2、与内存量息息相关:内存越大,耗时越长
3、info:latest_fork_usec :监控
改善fork
1、优先使用物理机或者高效支持fork操作的虚拟化技术
2、控制redis实例最大的可用内存
3、合理分配liunx内存分配策略
4、降低fork策略,比如放宽aof的触发机制,不必要的全量复制
2、进程外的开销
1、CPU
开销,RDB和AOF文件生成
优化:不做CPU绑定,不要CPU密集型部署
2、内存
开销:fork内存开销,copy-on-write
优化:不允许每次都写
3、硬盘
开销:AOF和RDB文件写入,可以结合iostat,iotop分析
优化
1、不要和高硬盘服务器部署在一起,存储服务,消息队列
2、no-appendfsync-on-rewrite=yes
3、根据写入量决定磁盘类型,ssd
4、单机多实例持久化文件目录可以考虑分盘
3、AOF追加阻塞
流程
1、主线程开启AOF缓存并且存放到存放到AOF缓存区
2、AOF缓存区同步线程
3、AOF同时对比fsync时间
4、如果大于2秒则阻塞,如果小于则通过
4、单机多实例部署
Redis的事务 与 锁策略
为什么要用事务
Redis的事务概念、简介
Redis事务的本质是
一组命令的集合
Redis中的事务指单独的隔离操作
事务支持一次命令执行多个命令,一个事务中所有的命令都会被序列化
没有回滚机制,事务中错误的命令无法执行,正确的命令会全部执行
结合pipeline
Redis的事务特点
一次性
顺序性
在事务执行过程中,会按照顺序执行
事务中所有命令都会按顺序执行,不会被其他命令打断
排他性
主要作:串联多个命令防止其他命令打断
没有隔离级别的概念
隔离性
没有隔离级别的概念
批量操作在事务提交前放入魂村队列,并不会被实际运行
⭐Redis的事务不保证原子性
Redis中的单条命令是原子性执行的,但事务不保证原子性,且没有回滚
事务中的任意命令执行失败,其余的命令仍会被执行
事务不保证原子性
不能保证原子性
Redis单条命令是保证原子性,但是事务不保证原子性
一条命令执行错误,其他命令正常执行
一条命令在编译时出错,所有命令都不执行
命令原子性
muli开启事务,将命令放入队列,exec执行所有命令,discard放弃事务
怎么保证原子性
分布式锁
setnx expeir
每次使用事务都需要手动开启,声明周期到执行或者放弃结束
Redis的事务命令、事务用法、常用命令、相关命令
multi
multi
声明事务
开启事务、开始事务
标记事务的开始
标识一个事务的开始
标记一个事务块的开始
命令入队
命令入队(一个或多个命令)
输入命令
exec
执行事务
执行某事务块内的所有命令
所有命令在事务中,并没有直接执行!只有发起执行命令时,才会执行!exec
redis单线程操作,exec的指令谁先来,就先执行哪个线程的操作。并不会因为mutli先来就先执行。
执行事务中所有在排队等待的指令并将连接状态恢复到正常
当使用WATCH 时,只有当被监视的键没有被修改,且允许检查设定机制时,EXEC会被执行
示例1
声明并执行事务
代码
watch k1 k2
watch命令
watch(监视)
watch:观察并撤销事务
乐观锁
实现乐观锁
CAS乐观锁
CAS乐观锁
悲观锁(Redis不支持)
watch xx
获取最新的值version,监视,select version 获取版本
watch key
watch
watch key…
观察key,若该key的value值被修改,当exec时 ,不执行。
监视一个或多个key,如果在事务执行之前这些被监视的key被其他命令修改,这该事务则被取消
监视key,如果事务在被执行前,被监视的key被改动,则事务执行失败
标记所有指定的 key 被监视起来,在事务中有条件的执行(乐观锁)
redis监视成功
redis监视成功
监控 Watch
子主题
示例3
利用两个客户端演示watch命令
代码
discard
discard(取消事务)
放弃事务、取消事务、放弃执行事务
事务队列中命令都不会被执行
刷新一个事务中所有在排队等待的指令,并且将连接状态恢复到正常。
如果已使用 WATCH,DISCARD 将释放所有被 WATCH 的 key。
实例2
取消事务
代码
unwatch
unwatch xx
如果事务执行失败,就先解锁
取消watch对所有key的监控
取消watch命令对所有key的监视
Redis事务的相关问题、异常
命令有错(相当于编译异常)
如果在事务队列中出现编译异常(语法错误),则执行exec命令,所有的命令都不会执行
事务整体都不会有效
事务队列存在运行错误(运行时异常)
如果在事务队列中出现运行时异常,则执行exec命令,错误命令抛出异常,其他命令正常执行
有错误的命令不生效但是不影响事务中别的命令
事务可能遇到的问题
在执行exec之前发生错误
在执行exec之后发生错误
Redis中的锁策略
简介
悲观锁(Redis不支持):假设当前操作很大几率会被打断
乐观锁:假设当前操作不会被打断,做操作前不会锁定资源,万一被打断,则操作被放弃
通过watch实现乐观锁,多读少写,unwatch命令可以取消加锁
watch命令
事务之前执行了watch(加锁),在exec/discard命令执行后锁会自动释放
可以使用watch做乐观锁操作(失败了就unwatch,然后重复操作,达到自旋效果)
redis的缓存并发竞争问题是什么?如何解决这个问题?
了解Redis事务的CAS方案么?
了解Redis事务的CAS方案么?
多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了。
或者多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据就错了
Redis自己就有天然解决这个问题的CAS类的乐观锁方案
解决:使用分布式锁
如图
1.使用分布式锁,确保同一时间内,只有一个系统的实例在操作某个key,别人都不允许写
zookeeper,redis
2.每次要写之前,要先判断一下当前这个value的时间戳是否比缓存里的value时间戳要大,如果更旧,那么就不能用旧数据覆盖新数据
Redis发布订阅、消息订阅
什么是发布订阅?
消息订阅
消息订阅
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息
简介:消息订阅是进程间的一种消息通信方式,即发送者发布消息(pub),订阅者(sub)接收消息,Redis支持消息订阅机制。
Redis客户端可以订阅任意数量的频道’
发布订阅模式
订阅频道
按规则订阅频道
订阅/发布消息图
订阅/发布消息图
使用场景
场景:
1、实时消息系统!
2、实时聊天!(可以将频道当聊天室,将消息回显示给所有人即可!)
3、订阅、关注系统都是可以的!
1、实时消息系统!
2、实时聊天!(可以将频道当聊天室,将消息回显示给所有人即可!)
3、订阅、关注系统都是可以的!
可以做好友动态、聊天室等等
实现原理
子主题
命令
publish/subscribe
publish 频道 消息:向指定频道发布消息
subscribe 频道:订阅频道
消息的发布和订阅
+ 先进行订阅
subscribe ooxx
+ 再进行发布
publish ooxx helloqiujing!
+ 所有订阅ooxx的客户端都会收到相应的消息
helloqiujing!
subscribe ooxx
+ 再进行发布
publish ooxx helloqiujing!
+ 所有订阅ooxx的客户端都会收到相应的消息
helloqiujing!
子主题
# 订阅一个或多个符合给定模式的频道
psubsribe pattern [pattern......]
# 查看订阅与发布系统状态
pubsub subcommand [argument [argument ...]]
# 将信息发送到指定的频道
publish channel message
# 退订所有给定模式的频道
punsubscribe [pattern......]
# 订阅给定的一个或者多个频道的信息
subscribe channel [channel ...]
# 指推定给定的频道
unsubscribe [channel [channel...]]
psubsribe pattern [pattern......]
# 查看订阅与发布系统状态
pubsub subcommand [argument [argument ...]]
# 将信息发送到指定的频道
publish channel message
# 退订所有给定模式的频道
punsubscribe [pattern......]
# 订阅给定的一个或者多个频道的信息
subscribe channel [channel ...]
# 指推定给定的频道
unsubscribe [channel [channel...]]
订阅/发送实例
先订阅频道
然后新开窗口往频道里发布内容
此时发现之前订阅了xiaoye这个频道的窗口自动监听了频道消息
订阅/发送
Redis集群方案、分布式解决方案、高可用
为什么要集群 ?
单机、单节点、单实例会存在哪些问题?
1.单点故障
2.数据量大,容量有限
3.压力--soket连接或计算的压力
性能
扩展
可用性
AKF介绍
AKF原则
一句话就能说明
通过加机器来解决容量和可用性的问题。
重点就在于加机器,先来看下面这张图:
X轴
X轴:水平复制,增加机器,单体系统多运行几个实例,做集群加负载均衡的模式;全量数据的克隆。
Y轴
Y轴:按照功能、业务拆分,如商品管理、订单管理、用户管理,将其分散在不同的实例上。
Z轴
Z轴:Y轴也承担不了大量的数据,进行数据分区。
大量的数据
比如一个机器承方了所有用户信息,只有4G的内存,
但是现在用户信息就有8G内存,就要Z轴扩展
比如按地区北京、上海或者按用户id尾号等再进行拆分。
AKF所带来的问题
数据一致性
强一致性
同步阻塞:当主库操作完成后,阻塞等待备库操作完成后才会返回,但是会破坏服务的可用性
强一致性会破坏服务的可用性
非强一致性
+ 异步方式:当主库完成指令后,异步的交给备库执行相应的操作,这种不会破坏服务的可用性,但是会丢失数据。
最终一致性!!
最终一致性!!
+ 可增加中间件的方式,如使用kafka,来存储异步的消息,备库reis来进行消费,保证了数据的完整性,最终会达到数据一致性。
(redis并没有使用这种方式)
故障转移
Redis主从复制、主从模式
哨兵模式 Sentinel
过半机制
学习地址
https://blog.csdn.net/asd13518662/article/details/116980800
Redis集群方案、分布式解决方案
Redis主从复制、主从模式
简介、概念、主从概念
Redis主从复制
主从
主从复制
Redis主从复制
区别于主备概念
备机不会参与业务,当主库宕机之后,才会使用备机
特点
将一台redis服务器的数据,复制到其他的redis服务器
主从复制,读写分离(master/slave机制)
客户端可以访问主机,也可以访问从库
为了读写分离,提高Redis性能;保证数据安全
主节点(master/leader)
Master以写为主
主(master)redis以写操作为主
而写只需要分给主机,就能缓减服务器压力
一主多从,主机宕机后,需要手动更改ip确定新的主机
主机负责写
一个主机(master)
从节点(slave/follower)
读的操作都分给从机上
多个从机(salve)
一个从机只能有一个master
一般从库上不会发生写操作,只会有读操作
Slave以读为主
从(slave)redis以读为主
从机从主机中同步复制数据,只读。
从机负责读
主从之间自动同步数据
主机数据更新后根据配置和策略,自动同步到备机的master/slave机制
从机从主机中同步复制数据,只读。
主机中所有的数据和命令,都会自动被从机保存
数据的复制都是单向的
数据流通是单向的,只能是master到salve
数据的复制都是单向的,只能由主节点到从节点
主从复制的细节
当主机断了,从机仍然运行连接主机,但是没有写操作,这个时候,如果主机回来了,从机依旧可以直接获取到主机写的信息
如果使用命令行,来配置的主从,这个时候如果重启,就会变成主机!主机变为从机,立马就会从主机中获取值
主从级联模式
如果从库数量过多,会导致主库忙于fork子进程,进行数据全量同步,
使用主从级联模式分担主库全量复制时的压力
环境搭建与主从复制的配置
info replication 查看当前库的信息
info replication
记住命令info replication
info replication
查看本机的复制信息
(常用的Redis命令)
查看当前主从状态信息
查看当前redis的主从策略
可以直到redis对应的一些信息,比如是主库,还是从库
以配置文件启动redis,查看后台进程
后台进程
复制配置文件,redis.conf
在一台机器上启动多个redis实例。
需要将默认的redis.conf文件复制一份
然后修改一下对应的设置,保证多个redis实例不出现共享数据就可以了
两种主从建立方式
临时建立:执行slaveof ip:port命令
默认情况下,每台redis服务器都是主节点
需要配置从机就可以了,主机不用配置
集群命令,通过命令实现
只配置从库,不用配置主库(配置从机,配从不配主,找老大)
在从机中配置,上面认老大的slaveof是命令式的,一次性的
⭐Redis默认自己就是主库,所以只需要配置从库
在从机上输入命令SLAVEOF 【主机的ip】 【主机的端口号】
slave从机配置
bind 127.0.0.1
port 6380
dbfilename dump80.rdb
slaveof 127.0.0.1 6379
port 6380
dbfilename dump80.rdb
slaveof 127.0.0.1 6379
salveof命令
slaveof
salveof ip port
salveof ip port
SlaveOF host port
host
主库的ip
port
就是主库的端口
从机需要跟随的老大
salveof no one
表示不是某个主机的从节点
执行该命令去除主从配置
永久建立:配置master配置文件
master主机配置
bind 127.0.0.1
port 6379
dbfilename dump.rdb
port 6379
dbfilename dump.rdb
通过配置redis.conf里面配置REPLICATION
修改配置文件、一般修改配置redis.conf
注释bind绑定ip
/bind搜索
端口
端口 port
/port搜索
守护线程开启
/daemonize搜索
pid修改
pid 名字
/pidfile搜索
日志文件修改
log日志文件名字
/logfile搜索
dbfilename修改
/dbfilename搜索
dump.rdb文件名字
appendonlyfile.aof文件名字
最好使用配置文件配置实现
通过配置实现
最好使用配置文件配置
两种配置的区别
1、使用命令进行主从配置,不需要重启redis服务器,但是不方便管理
2、使用配置进行主从配置,需要重启redis服务器,但是方便管理
salve-read-only yes
slave-read-only yes
设置从节点只当作读的操作
主从复制的配置
读写分离,主机读写,从机从主机中同步复制数据,只读。
一主多从,主机宕机后,需要手动更改ip确定新的主机
主要是为了高并发,缺点是难扩容,整个集群的容量受限于某台机器内存容量
适用场景、使用场景
单台Redis的最大使用内存不要超过20G
建议至少一主二从的配置,一主一从哨兵机制会出错
电商网站上的商品,一般都是一次上传,无数次浏览
主从复制的优点、主要作用
数据热备份、数据冗余
实现数据热备份,是数据持久化的另一种操作
主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
故障恢复
故障恢复,当某一服务挂了后,可以从节点进行数据恢复
当主节点出现问题时,可以 由从节点提供服务,实现快速的故障恢复;实际上时一种服务的冗余
充当数据副本使用,避免数据丢失
负载均衡、读写分离
负载均衡,主机写,从机读,分担负载
在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载
写redis数据时,应用连接主节点;
读redis数据时,应用连接从节点
扩展读的性能
在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量
高可用基石
高可用基石,主从复制是哨兵机制和集群的基础
除了上诉的作用外,主从复制还是哨兵和集群能够实施的基础,因此主从复制时redis高可用的基础
承担读压力
master读写操作,slave只有读操作
非阻塞
Redis使用默认的`异步复制`,其特点是低延迟和高性能,是绝大多数 Redis 用例的自然复制模式。
主从同步期间,master仍然提供查询、修改等操作服务,slave仍然提供查询操作服务
主从复制的缺点、主从复制的不足、问题
缺点是难扩容,整个集群的容量受限于某台机器内存容量
运维问题
1、主从分离
1、将主机的读操作分摊给从节点
读写分离可能遇到问题
1、复制数据延迟(阻塞可能会造成)
复制数据延迟
2、读到过期数据
读到过期数据
3、从节点故障
从节点故障
2、主从配置不一致
maxmemory配置不一致
可能丢失数据
maxmemory不一致可能会丢失数据
数据结构优化参数不一致
数据结构优化参数不一致
内存不一致
可能会造成主节点和从节点内存不一致
3、规避全量复制
1、第一次全量复制不可避免
通过数据分片设置不要设置过大,进行低峰复制
第一次全量复制
不可避免
优化:小主节点(小分片),低峰
2、节点运行ID不一致
节点runId不匹配导致复制
主节点重启(runId会发生变化)
主节点重启后runId变化
优化:故障转移(哨兵、集群)
故障转移
3、复制积压缓存区不足
复制积压缓冲区不足
网络中断后无法进行部分复制
网络中断,部分复制无法满足
优化:rel_backlog_size(默认1m)
增加复制缓冲区的配置,进行网络增强
4、规避复制风暴
1、单主节点复制风暴
主节点重启,从节点复制
更换复制徒
一个主节点下面有三个从节点,更改为一个主节点只有一个从节点1,剩下的从节点都挂载在从节点1中。
2、单机器复制风暴
主节点分布在多个机器
主节点重启,多个从节点复制
优化:更换复制拓扑
常见问题
1、从机完全复制主机的信息
从机是从头开始复制,还是从切入点开始复制
从头复制
2、从机无法进行写操作
3、主机shutdown以后,从机原地待命
主机shutdown后,从机是上位还是原地待命
原定待命,不会变成主机
从机是否可以写 set?
不能 从机只能读get
主从复制的一些问题
由于所有的写操作都是现在Master上操作,然后同步更新到Slave上
所以从Master同步到Slave上机器有一定的延迟,当系统很繁忙的时候,
延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重
所以从Master同步到Slave上机器有一定的延迟,当系统很繁忙的时候,
延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重
所有的slave节点的数据复制和同步都由主节点来处理,造成master压力大
主机重新启动后,从机是否能够顺利复制
可以
从机shutdown后,情况如何
从机挂了之后,会丢失之前的主从关系,需要重新设置一次主从关系才行
在配置文件里面可以永久的设置主从关系(slaveof),挂了重启再启动,主从关系还会保持
主从复制原理、主从复制数据同步的原理、主从复制流程
前提
Master和Slave都会维护一个offset和run id ,Slave每秒都会上报自己的offset给master
Master记录在backlog(针对增量复制)中,这样才能知道双方数据是否一致
Slave发送run id 和offset到Master,Master根据情况返回信息(增量/全量)
全量复制、复制过程的核心原理(简述)
full resynchronization
开始全量复制时,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。
RDB文件生成完毕之后,master会将这个RDB发送给slave
slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。
然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
全量复制、复制过程的核心原理
主从同步
第一阶段
主从库建立连接,协商同步过程,为全量复制做准备
第二阶段
主库将所有数据(RDB)同步给从库,从库收到数据后,在本地完成数据加载
第三阶段
主库把第二阶段执行过程中新收到的写命令(repl buffer),再发送给从库
核心原理
1.【s -> m】psync runId offset
slave向master发送psync指令;
Slave启动时会向Master发送sync指令、psync指令(请求同步)
每次从机联通后,都会给主机发送 sync 指令
当从库和主库建立MS(Master-Slave)关系后,会向主库发送sync命令、psync指令(请求同步)
从机一旦连接,就会向主机发送一个sync命令、psync命令(请求同步)
psync与sync(旧指令)区别
psync支持部分重同步
在主从复制过程中slave断线,再次连接不需要全量重新复制同步。
2.【m -> s】+FULLRESYNC {runId} {offset}
主机就会发送全部数据给从机,完成同步
3.【s】save masterInfo
4.【m】bgsave / write repl_back_buffer
Master收到后,通过bgsave保存快照,同时将后续的命令存到缓存中
master受到psync/sync指令后,开始执行BGSAVE操作,生成RDB文件并使用缓冲区记录此后执行的所有写命令;
主库Master接收到psync/sync命令后会开始再后台保存快照,并将期间接收到的命令保存
5.【m -> s】send RDB
BGSAVE完成后,向slave发送RDB文件,并继续记录写命令;
Master将RDB发给Slave
redis2.8版本后,支持向slave直接发送RDB文件,不需要将其加载到内存中发送。
当快照完成后,主库会将快照和所有的缓存的写命令发送给从库
主机立刻进行存盘操作,发送 RDB 文件给从机
6.【m -> s】send buffer
master每执行一次写命令,都会向slave发送相同的命令,使其执行相同操作
之后每次主机的写操作,都会立刻发送给从机,从机执行相同的命令
最后master会将内存中的写命令,同步给Slave,Slave收到后再执行一遍
当主库每当接收到写命令时会将命令发送至从库,保持数据一致性
7.【s】flush old data
slave加载文件,放弃旧数据
slave服务在接受到数据库文件数据后,将其存盘并加载到内存中
Slave收到文件后先写入到本地磁盘,然后在从本地磁盘加载到内存中
从库接收到后,会载入快照并执行存储的缓存命令
从机收到 RDB 文件后,进行全盘加载。
8.【s】load RDB
slave加载完毕后,执行来自于master缓冲区记录写命令
触发时机
Slave从机第一次启动时
Master重启时
全量复制开销过程、开销大
1、【m】bgsave时间开销
bgsave时间
2、【m】RDB网络传输开销
RDB的文件网络传输时间
3、【s】清空数据时间开销
从节点清空数据时间
4、【s】加载RDB时间开销
从节点加载RDB时间
5、【s】AOF重写时间(可能的)
可能存在AOF重写的时间
增量复制、部分复制的核心原理
1.【s -> m】psync runId offset
Master根据Slave发送的同步请求中的offset
slave发送psync和加载RDB文件时,会存在offset偏移量
2.【m -> s】CONTINUE
3.【m -> s】send partial data
在backlog中查询部分丢失的数据,发送给Slave
master只会发送增量数据。
过期key的处理
Slave不会处理过期key,只会等待Master的过期通知
注意事项
要用redis运用与工程中,只使用一台redis是万万不能的(因为会出现宕机问题,导致数据丢失,因此一般会准备3太,一主二从)
从结构上,单个redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大
从容量上,单个redis服务器内存容量有限,就算一台redis服务器内存容量为256G,也不能将所有内存用作与redis存储内存。
一般来说,单个redis最大使用内存不应该超过20G
哨兵模式、哨兵机制 Sentinel
哨兵模式是什么 ?
哨兵模式sentinel
哨兵集群
Redis-Sentinel
Redis-Sentinel是Redis高可用的实现方案
图解哨兵模式
运行图例 -图解哨兵模式
以前:主从复制模式
当主机断了,从机可以通过命令手动变为主节点,slaveof no one,变为主节点;
但当这是主机又回来了,那就只能重新配置,重新连接
如果用主从架构部署,其实就是加上哨兵就可以了,就可以实现任何一个实例宕机,自动进行主备切换。
现在:特殊的主从复制模式
哨兵模式是一种特殊的模式
首先redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,他会独立运行。
其原理就是哨兵通过发送命令,等待redis服务器相应,从而监控运行的多个redis实例
(自动选举老大,只要发现老大没了,就会选举新的老大)
哨兵模式的优点
具备主从所有优点
主从可以自动切换,系统更健壮,可用性更高
基于主从复制模式,所有的主从复制的优点,它全都有
主从自动切换,故障可以转移,系统可用性更好
哨兵模式就主从模式的升级,手动到自动
哨兵模式的缺点、不足
不好在线扩容
不好在线扩容
集群容量一旦达到上限,在线扩容就特别麻烦
哨兵模式的配置十分麻烦
实现哨兵模式,配置很麻烦
哨兵模式的四大主要功能
消息通知、客户端通知
提醒(Notification)
消息通知: redis 实例故障,发消息给管理员
如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
当被监控的某个 Redis出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知
配置中心
配置中心: 转移发生,通知client新的master地址
如果故障转移发生了,通知client客户端新的master地址。
集群监控、故障发现
监控(Monitoring)
通过发送命令,让redis服务器返回监控其运行状态,包括主服务器和从服务器
哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常
sentinel monitor mymaster 127.0.0.1 6379 2
监控主master127.0.01端口号为6379的进程,判断为失效至少需要2个Sentinel 同意
sentinel monitor mymaster 127.0.0.1 6379 2
监控主master127.0.01端口号为6379的进程,判断为失效至少需要2个Sentinel 同意
集群监控:负责监控master 和slave 进程
监控主节点从节点是否正常运行
负责监控redis master和slave进程是否正常工作。
自动故障转移
当确认主节点宕机后,在从节点中选一个座位主节点,将
其他从节点连接到新的主节点上通知客户端最新的地址
其他从节点连接到新的主节点上通知客户端最新的地址
检测master状态,若异常则选取一个从机升为主机,原主机降为从机
如果master节点挂了,会自动转移到slave节点上
自动故障迁移(Automatic failover)
哨兵是一个分布式系统,监控主从架构中的节点通过 自动故障转移 保证集群的高可用
当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master
当哨兵检测到master宕机,会自动将slave切换程master,然后通过发布订阅模式,通知其他的从服务器,修改配置文件,让他们切换主机
故障转移: master 宕机,自动转移slave
主从库自动切换,解决主从复制模式下故障转移问题
多个哨兵监控主库,超过指定数量个哨兵发现主库不回应,就投票选举新的主库
投票算法
保证了集群的高可用,但仍有容量上限问题
故障转移
1、客户端高可用观察
2、服务端日志分析:数据节点和sentinel节点
故障转移流程
1、从slave节点选出一个”合适的“ 节点作为新的master节点
2、对上面的slave节点执行slaveof on one命令让其成为master节点
3、向剩下的slave节点发送命令,让他们成为新master节点的slave节点,复制规则和parallel-syncs参数有关
4、更新对原来master节点配置,并保持对其关注,当宕机的机器恢复后就命令他复制新的master节点
如何选择合适的slave节点
1、选择slave-priority(slave节点优先级)最高的slave节点,如果存在则返回,如果不存在则继续
2、选择复制偏移量最大的slave节点(复制的最完整),如果存在则返回,不存在则继续
3、选择runId最小的slave节点
哨兵+主从复制的部署架构,是不会保证数据零丢失的
哨兵+redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性。
哨兵+redis主从部署,不保证数据零丢失,只保证高可用
消息丢失
min-slaves-to-write 1
min-slaves-max-lag 10
哨兵模式的底层工作原理
sentinel 集群可看成是一个 ZooKeeper 集群
哨兵模式的三个定时任务
1、每10秒,每个sentinel都会对master和slave执行info
每10秒,sentinel对m/s执行info
发现slave节点
发现、确认主从关系
2、每2秒,每个sentinel通过master节点的channel交换信息(pub/sub)
每2秒,sentinel通过master的channel交换信息
master频道:__sentinel__:hello
通过_sentinel_hello;进行频道的交互
交换对节点的看法、以及自身信息
交互对节点的看法和自身的信息
3、每1秒每一个sentinel会对其他sentinel和redis进行ping
每1秒,sentinel对其他sentinel和redis执行ping
心跳检测,失败判定依据
心跳检查,失败判断依据
说明
Redis-Sentienl通过三个定时任务,实现了Sentienl节点对三类节点的监控
主节点
从节点
其他Sentinel节点
多哨兵模式、哨兵节点至少需要三个实例
Redis-Sentinel的sentinel节点(哨兵节点),最好是>=3个,并且数量最好是基数个
哨兵至少需要三个实例,来保证自己的健壮性。
至少需要3个实例,保证健壮性
如果哨兵只部署了两个,且都在redis节点上
如果哨兵集群仅仅部署了两个哨兵实例,quorum=1-->几个哨兵同意认为master宕机可以进行切换。
同时S1,S2会选举出一个哨兵来执行故障转移的操作。
同时,需要majority(几个哨兵同意进行故障转移),也就是大多数哨兵都是运行的。
同时S1,S2会选举出一个哨兵来执行故障转移的操作。
同时,需要majority(几个哨兵同意进行故障转移),也就是大多数哨兵都是运行的。
2个哨兵的majority就是2,两个哨兵都运行着,就可以允许故障转移
(原则:3的majority就是2,5的majority就是3,4的majority就是2)
一台机器挂掉,只剩一个哨兵,所以两个哨兵节点的故障转移没法执行了
经典的3节点哨兵集群
设置:quorum=2,majority
如果MASTER1所在机器宕机了,那么三哨兵还剩两个,s2和s3可以一致认为master宕机,然后选举出来一个进行故障转移
同时3个哨兵majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移。
同时3个哨兵majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移。
多个哨兵监控主库,超过指定数量个哨兵发现主库不回应,就投票选举新的主库
投票算法
保证了集群的高可用,但仍有容量上限问题
尽可能的在不同服务器上配置sentinel节点(哨兵节点)
如有超半数哨兵故障,会导致无法执行主从切换,但能进行主库主观下线
Redis-Sentienl数据节点(哨兵节点)和普通的数据节点没有区别
哨兵也是分布式的,原因如下
1.故障转移时,判断一个master节点是否宕机,需要大部分哨兵都同意才行,涉及到分布式选举的问题。
2.即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
多哨兵模式
一个哨兵进程对redis服务器进行监控,可能会出现问题
可使用多个哨兵进行监控,各个哨兵间会互相进行监控,这样就形成多哨兵模式
过半机制
当主机宕机后,从库必须投票选举,过半的数量认为主机宕机,才生效。
节点数量推荐奇数
1.从成本角度看:3台和4台的可允许失效错误的机器数都是1,还不如用3台;
2.从概率看:4台节点数比3台节点数,更容易发生1台机器生效错误的问题。
quorum和majority之间的关系
每次一个哨兵要做主备切换
quorum最好配置所有哨兵的一半+1
配置quorum 主库客观下线最小同意人数
首先,需要quorum数量的哨兵认为odown,然后选举出来一个哨兵做切换。
然后,这个哨兵还得的到mojority个哨兵的授权,才能正式执行切换。
如果quorum<mojority
比如5个哨兵,mojority就是3
quorum设置为2,那么3个哨兵授权就能切换。
但是如果quorum>mojority
那么就以quorum个数量的哨兵授权才能切换。
哨兵和slave集群的自动发现机制
通过Redis 发布/订阅机制,发现其他哨兵并组成集群
哨兵之间的互相发现,是通过redis的pub/sub系统实现的,
每个哨兵都会往_sentinel_:hello 这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他哨兵的存在。
每隔2秒钟,每个哨兵都会往自己监控的master+slaves对应的_sentinel_:hello channel发送消息,内容为自己的host,ip,runid还有master的监控配置
每隔哨兵也会监听自己监控的每个master+slaves对应的_sentinel_:hello channel ,然后去感知到同样在监听这个master+slaves的哨兵的存在,还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步。
简化版本的工作机制
哨兵进程检测redis服务进程,会间隔进行心跳检测确定redis服务进程是否存活
一旦主机挂机了,就会进行故障转移---failover,重新选取主机
就算挂掉的主机回来了,也只能作为从机继续服务了
详细版本的工作机制
①Sentinel发送ping命令
每个 Sentinel 以每秒一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个PING命令
每个 Sentinel 以每秒钟一次的频率向它所知的 Master,Slave 以及其他 Sentinel 实例发送一个 PING 命令
②Sentinel标记异常实例为主观下线
如果一个实例,距离最后一次有效回复 PING 命令的时间,超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线;
实例(instance)
③Sentinel确认Master 的确进入了主观下线状态
如果一个主服务器被标记为主观下线, 那么正在监视这个主服务器的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。
如果一个 Master 被标记为主观下线,则正在监视这个 Master 的所有 Sentinel ,要以每秒一次的频率,确认Master 的确进入了主观下线状态
④多个Sentinel节点确认Master进入主观下线状态,标记Master为客观下线
如果一个主服务器被标记为主观下线, 并且有足够数量的 Sentinel在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。
当有足够数量的 Sentinel在指定的时间范围内,确认 Master 的确进入了主观下线状态, 则 Master 会被标记为客观下线
Sentinel 实例/节点需要
大于等于配置文件指定的值
(至少要达到配置文件指定的数量)
⑤在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有 Master,Slave 发送 INFO 命令
⑥当 Master 被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的
频率会从 10 秒一次改为每秒一次 ;
频率会从 10 秒一次改为每秒一次 ;
⑦若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除;
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除
总结:
挑选出新的master后,sentinel向原master的从服务发送slaveof新master的命令,
当下线的原master上线时,sentinel会发送slaveof命令让其成为新master的slaver
故障转移流程
一、【故障发现】多个sentinel发现并确认master有问题,发现Master宕机
哨兵模式在节点失败时,分为两种下线方式、sdown和odown转换机制
图解什么是主观下线, 什么是客观下线
解释什么是主观下线, 什么是客观下线
主观下线
每个sentinel对redis节点失败的偏见
Subjectively Down
简称 SDOWN
sdown主观宕机:就一个哨兵如果自己觉得master宕机了。
sdown达成条件很简单,如果一个哨兵ping一个master,超过is-master-down-after-milliseconds指定的毫秒数后,就主观认为master宕机。
指的是当前 Sentinel 实例对某个 redis 服务器做出的下线判断。
客观下线
所有sentinel对redis节点失败达成共识(超过quorum的统一)
Objectively Down
简称 ODOWN
odown客观宕机:如果quorum数量的哨兵都觉得一个master宕机了。
指的是多个 Sentinel 实例在对 Master Server 做出SDOWN 判断,
并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后,得出的 Master Server 下线判断,然后开启 failover.
sdown转换成odown条件很简单
如果一个哨兵在指定时间内,收到了quorum个指定数量的其他哨兵也认为master宕机了,
那么就认为是odown,客观认为master宕机。
二、【sentinel领导选举】选举出一个sentinel作为领导、sentinel领导选举
原因:只有一个sentinel节点完成故障转移
集群通过投票(获取半数以上选票)选举Leader执行主从切换
选举
实现:通过sentinel is-master-down-by-addr命令竞争领导者
1、每个做主观下线的sentinel节点向其他sentinel节点发送命令,要求将它设置为领导者
2、收到命令的sentinel节点如果没有同意通过其他sentinel节点发送的命令,那么将同意该申请,否则拒绝
3、如果sentinel节点发现自己的票数已经超过sentinel集合半数且超过quorum,那么自动成为领导者
4、如果此过程有多个sentinel节点成为领导者,那么等待一段时间重新选举
三、【选master、选主库】选出一个slave作为master, 并通知其余slave
对这个slave执行slaveof no one
哨兵从服务器列表中挑选Master 、slave-master选举算法、选新 master 的原则、新master选取策略
筛选与过滤
(1)过滤掉不在线和响应慢的服务器
去掉已下线和历史网络连接状态不好的从库
(2)过滤掉与原Master断开时间最久的
根据 master断开连接的时长,先排除不合适的从库
如果一个slave跟master断开连接的时长减掉master宕机的时长已经超过了down-after-milliseconds的10倍,那么slave就认为不适合选举为master
排序
对slave进行排序,选新 master 的原则,需要考虑以下四个信息:
(1)slave优先级最高的
选slave-priority最高的
最后比较优先级priority
按照slave优先级进行排序,
依据优先级,配置文件中配置slave-priority属性
配置文件里设置的slave-priority越小,优先级越高
(slave-priority配置项)
优先级最高的从库
(2)复制offset最大的
偏移量最大
复制offset大小
选复制偏移量最大的
和旧主库同步程度最接近的从库(slave_repl_offset和master_repl_offset)
slave里复制主节点的offset越靠后,说明复制的数据越多,优先级就越高
如果两个服务器优先级一致,那么回去查看从服务器中数据的offset,
offset说明数据最新,选出offset大的服务器为Master
获取原master数据最多的从机
(3)run id比较小的slave
如果上面两个都一样,那么就选一个run id比较小的slave
run id、选runId最小的
run id号最小的从库
选择runid最小的
(每个redis实例启动后都会随机生成一个40位的runid)
新Master诞生、Slave配置的自动纠正
哨兵向选举出的新Master发送指令,断开与旧Master的连接
把新Master的ip地址同步到其他Slave节点
如果哨兵选举了一个新的master,哨兵会去让剩下的slave修改他们的配置,连接到新的master上面去
configuration epoch
哨兵会对一套master+slaves进行监控,有相应监控的配置
执行切换的那个哨兵,会从要切换到的新master(slave->master)那里的到一个configuration epoch,
这就是一个version号,每次切换的version号都必须是唯一的。
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout的时间,
然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号
configuration传播
哨兵完成主从切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,通过之前说的pub/sub消息机制channel。
这里上面的version 就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成了一次新的主从切换之后,新的master配置是跟着新的version号的
其他的哨兵如果发现版本号大于自己目前存储的版本号,就会去更新自己的master配置
四、【通知】通知客户端主从变化
五、【老master】等待老的master复活成为新master的slave
sentinel会保持对其关注
以前的Master如果重连了,那么以前的Master会变成新Master的Slave
客户端流程
【sentinel集合】 预先知道sentinel节点集合、masterName
【获取sentinel】遍历sentinel节点,获取一个可用节点
【获取master节点】get-master-addr-by-name masterName
【role replication】获取master节点角色信息
【变动通知】当节点有变动,sentinel会通知给客户端 (发布订阅)
客服端从哨兵订阅消息,进行主库切换
JedisSentinelPool -> MasterListener --> sub "+switch-master"
sentinel是配置中心,而非代理!
客户端初始化时连接的是sentinel集合,而不是具体的redis节点,但是sentienl只是配置中心而不是代理
哨兵模式的安装、配置以及使用注意
1、配置哨兵配置文件:vim sentinel.conf
编写sentinel.conf
配置哨兵:sentinel monitor mymaster 127.0.0.1 6379 1
sentinel monitor [名称] 127.0.0.1(主机地址) 6379(主机端口) 1(开启主机选举)
2、最核心的配置,和最基础的
sentinel monitor myredis 127.0.0.1 6379 1
# sentinel monitor 被监控的名字 host port 1
后面的数字1,代表主机挂了,slave投票看让谁接替称为主机,票数最对的,就会称为主机
3、启动启动哨兵进程
启动哨兵:redis-sentinel sentinel.conf
redis-sentinel sentinel.conf
4、如果主机主机挂掉后,哨兵自动从从机中选择一个当主机
哨兵机制会在下次心跳检测到主机挂掉,故障转移,选取新主机
5、但当原主机回来了,只能当从机
之前的主机就算复活了,也只能当从机了,
一旦失去不再来,除非当前主机挂掉,重新选取才有机会
哨兵模式的全部配置
第一部分
第二部分
第三部分
哨兵模式的环境搭建 与配置项
1、配置主从节点
2、配置开启sentinel监控主节点
3、配置多台机器
4、详细配置节点
哨兵模式的常见运维
哨兵模式的充分测试与上线
对于哨兵+redis主从这种复杂的部署架构
尽量在测试环境和生产环境,都进行充足的测试和演练
建议充足测试
节点的上线和下线、上下线节点
主节点
上线主节点
sentinel failover 进行替换主节点上线
sentinel failover进行替换
下线主节点
sentinel failover <masterName>主节点下线
sentinel failover
从节点
上线从节点
slaveof命令,sentinel节点可以感知
slaveof
下线从节点
临时下线或者永久下线,需要考虑读写分离和原因
Redis-Sentienl读写分离可以依赖于Sentienl节点消息通知,获取Redis节点的数据状态变化
考虑是否做清理、考虑读写分离
sentinel节点
上线sentinel
参考其他sentinel节点启动
临时下线或者永久下线,需要考虑读写分离和原因
节点运维问题
机器下线(机器过保)
机器性能不足(比如CPU、内存等)
节点自身故障(网络不稳定等)
高可用的读写分离
从节点的作用
1、当作副本,并且是高可用的基础
2、扩展redis 读操作的能力
client关注slave节点资源池
关注三个消息
+switch-master: 从节点晋升
+convert-to-slave: 切换为从节点
+sdown: 主观下线
Redis Cluster集群模式
Redis Cluster集群模式是什么?
Redis集群、集群、集群模式
Cluster、cluster、cluster模式、redis集群(Redis cluster)
redis cluster、redis cluster架构、 redis cluster集群模式、Redis-Cluster集群
Redis Cluster集群模式的简介、优点与 特性
对Redis水平扩容
Sharding
分区
Redis Cluster是一种服务端Sharding技术
redis 3.0之后开始支持的
支持 redis集群(Redis cluster)
多master+读写分离+高可用
多主多从的架构
支持N个master node
可以在多台机器上,部署多个master(实例)
可以在多台机器上,部署多个redis实例master
每个master存储一部分的数据
每个实例master存储一部分的数据
每个redis实例可以挂Redis从实例
每个master node都可以挂载多个slave节点
每个master node可以挂载多个slave node(从实例)
读写分离的架构
每个master来说,写久写到master
读就从master对应的slave去读
高可用的架构
提供内置的高可用支持
部分master不可用时,还是可以继续工作的。
每个master都有slave节点
如果master挂掉,redis cluster这套机制,就会自动将某个slave切换成master
如果redis主实例master挂了,会自动切换到redis从实例顶上来。
去中心化配置
中心化
所有的节点都要有一个主节点
缺点
中心挂了,服务就挂了
中心处理数据的能力有限,不能把节点性能发挥到最大
特点
就是一个路由作用
去中心化
让每个主机都拥有转发能力
复制
主从复制 (异步):SYNC snapshot + backlog队列
快照同步
增量同步
无盘复制
wait 指令
集群架构
集群也使用了主从复制
只需要基于redis cluster去搭建redis集群即可
不需要手工去搭建replication复制+主从架构+读写分离+哨兵集群+高可用
分摊压力
实现扩容
Redis Cluster集群模式的缺点
key批量操作支持有限
多键操作不被支持
mget/mset 必须在同一个slot
key事务和lua支持有限
操作的key必须在同一个slot
多键redis事务不被支持,lua脚本不被支持
key是数据分区最小粒度
bigkey无法分区
复制只支持一层
无法树形复制
Redis Cluster集群模式的创建
原生
配置文件:cluster-enabled yes
启动: redis-server *.conf
gossip通讯:cluster meet ip port
分配槽(仅对master):cluster addslots {0...5461}
配置从节点:cluster replicate node-id
脚本
安装ruby
安装ruby环境
安装rubygem redis
安装redis gem
安装redis-trib.rb
配置文件
验证
cluster nodes
cluster info
cluster slot
redis-trib.rb info ip:port
Redis Cluster集群模式的工作原理
哈希槽(Hash Slot)什么?
slot(插槽)
引入哈希槽概念
采用slot的概念
通过哈希的方式
redis集群分区使用了哈希槽的概念
每个slot可以存储一批键值对
一个hash slot中会有很多key和value
槽位信息存储于每个节点中
Rax
一个Redis集群包含16384个插槽
默认16384
16384个
数据库中每个键都属于16384个插槽中的一个
一个Redis集群包含16384个插槽(hash slot)
有2^14(16384)个哈希槽
Redis 集群中内置了 16384(2^14) 个哈希槽
每个key通过CRC16校验后,再对16384取模,来决定落在哪个哈希槽上
分片
slots的定位:crc16(key) % 16384
key通过CRC16算法校验,并对16384取模,将数据分片,最后会将数据放到对应的哈希槽
每个key通过CRC16校验,并对16384取模,来决定落在哪个哈希槽上
集群使用公式CRC16(KEY)%16384来计算key属于哪个槽
每个key通过CRC16校验后,再对16384取模,来决定落在哪个哈希槽上
每个key通过CRC16校验后,再对16384取模来决定放置哪个槽
每个key通过CRC16校验后,并对16384取模来决定放置哪个槽
当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 CRC16算法算出一个结果,然后把结果对 16384 求余数
当往Redis Cluster中加入一个Key时,会根据CRC16(key) mod 16384计算这个key应该分布到哪个hash slot中
不同的key分散到不同的主节点上,每个主节点管辖一定的槽位
将kv映射到哈希槽中
结果对应0~16383之间的哈希槽
每个节点均分存储一定的哈希槽区间的数据
再将16384个slot槽,均分到master实例中
由于哈希槽个数一般远远大于实际生产的机器数量,所以这套预分区规则,可以满足所有的生产需求。
自动将数据进行分片,每个master上放一部分数据。
不同的key分散到不同的主节点上,每个主节点管辖一定的槽位
保证了高扩展,适用于数据量大,需要持续扩容
集群的每个节点负责一部分hash槽
每个 key 都会对应一个编号在 0-16383 之间的哈希槽,Redis会根据节点数量大致均等的将哈希槽映射到不同的节点。
计算槽位
cluster keyslot k
Redis Cluster集群模式分区规则优点
由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态
很容易添加或删除节点
如果需要添加节点,只需要各将旧节点的部分槽位移动到新节点上即可;
如果需要删除及诶单,只需要将被删除的节点槽位转移到其余节点即可;
Redis Cluster集群模式类比于表分区
单节点Redis
只有一个表
所有的key都放在这个表里
Redis Cluster集群模式
自动生成16384个分区表
插入数据时,会根据如上简单算法来决定key应该存在哪个分区
每个分区里有很多key
Redis Cluster集群模式分区规则 举例说明
案例1
当前集群有3个节点
节点 A 包含 0 到 5500号哈希槽.
节点 B 包含5501 到 11000 号哈希槽.
节点 C 包含11001 到 16384号哈希槽.
节点 B 包含5501 到 11000 号哈希槽.
节点 C 包含11001 到 16384号哈希槽.
案例2
假如redis集群里面能存放90个key,那么redis集群把90key平分到3个主机
redis对每个主机里面30个存储位置都编号,当应用连接到主机1上面时,发送一个写的命令
主机使用CRC16,算出槽号
如果槽号在1-30,可以直接操作主机1
如果槽号在31-60,那么redis会转发到主机2
如果槽号在61-90,那么redis会转发到主机3
如果再发一个命令set age 22,那么主机2使用CRC16,再算槽号,再转发
数据存储的核心算法
用不同的算法,就决定了在多个master节点时,数据如何分布到这些节点上去,解决这个问题
hash算法
来了一个key之后,计算hash值,然后对节点数量取模,取模结果一定小于节点数量,然后放到取模对应的节点上
但是如果某台master宕机了,就得把其他两台master的数据全拿出来重新取模计算,所有数据放到两台机器上。
高并发场景下时不可接受的。
并发,一台机器挂掉了,原来是对3取模存的数据,现在对2取模取数据,会导致所有请求全部无法拿到有效的缓存。
最大问题,一个master宕机那么大量的数据就需要重新计算写入缓存。
并发,一台机器挂掉了,原来是对3取模存的数据,现在对2取模取数据,会导致所有请求全部无法拿到有效的缓存。
最大问题,一个master宕机那么大量的数据就需要重新计算写入缓存。
一致性hash算法
master分布在一个圆环上
然后有一个key过来以后,同样也是计算hash值,然后会用hash值在圆环对应的各个点上
key落在圆环上以后,就会顺时针旋转去寻找距离自己最近的一个节点。
然后有一个key过来以后,同样也是计算hash值,然后会用hash值在圆环对应的各个点上
key落在圆环上以后,就会顺时针旋转去寻找距离自己最近的一个节点。
一致性hash算法保证,任何一个master宕机,只有之前在那个master上的数据会受到影响,因为朝着顺时针走,全部在之前的master上找不到了,master宕机了。会顺时针找到下一个master,也找不到。
1/n的流量,会瞬间涌入数据库中,重新查询一次。
1/n的流量,会瞬间涌入数据库中,重新查询一次。
缓存热点问题
可能集中在某个hash区间内的值特别多,那么会导致大量的数据都涌入同一个master内,造成master的热点问题,性能出现瓶颈
虚拟节点
给每个master节点都做了均匀分布的虚拟节点
然后这样的话,在每个区间内,大量的数据,都会均匀的分布到不同的节点内,而不是按照顺时针的顺序去走,全部涌入同一个master内。
然后这样的话,在每个区间内,大量的数据,都会均匀的分布到不同的节点内,而不是按照顺时针的顺序去走,全部涌入同一个master内。
redis cluster的hash slot算法
redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取对应的hash slot
redis cluster中每个master都会持有一部分的hash slot,比如有3个master,那么可能每个master持有5000多个hash slot
hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去,移动hash slot的成本是非常低的。
客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现。
redis cluster中每个master都会持有一部分的hash slot,比如有3个master,那么可能每个master持有5000多个hash slot
hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去,移动hash slot的成本是非常低的。
客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现。
任何一台机器宕机,不影响其他节点,因为key找的是hash slot,不是机器。
redis发现某台机器宕机,会把这台机器的slot快速的分配到其他的master上
Redis Cluster集群模式的hash slot算法
略
节点间内部通信机制
集中式元数据维护和存储
集群有很多元数据
故障的信息
master->slave之间的关系
hash slot->node之间的映射表关系
典型方案
基于zookeeper集中式元数据的维护和存储
典型代表
storm
分布式的大数据实时计算引擎,集中式的元数据存储架构
底层基于zookeeper(分布式协调中间件)的集群所有元数据维护。
Redis采用的是gossip协议
小道留言
每个 master自己都会维护一份完整的元数据
一个master有变更,就会把元数据发送给其他的master,其他master收到了就会更新一下自己的元数据。
goosip协议包含多种消息
ping
每个节点都会频繁的给其他节点发送ping,
其中包含自己的状态,还有自己维护的集群元数据,互相通过ping交换元数据。
ping消息深入
ping很频繁,而且要携带一些元数据,所以可能会加重网络负担
每个节每秒会执行10次ping,每次会选择5个最久没有通信的其他节点
当然如果发现某个节点通信延迟达到了cluster_node_timeout/2,那么立即发送ping,避免数据交换延时过长,落后的时间太长了
比如两个节点之间都10分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。
所以cluster_node_timeout可以调节,如果调节比较大,那么会降低发送消息的频率。
每次ping,一个是带上自己节点的信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换。
至少包含3个其他节点的信息,最多包含总节点-2个其他节点的信息。
pong
返回ping和meet
包含自己的状态和其他信息
也可用于信息广播和更新
meet
每个节点通过meet命令交换槽位信息
某个节点发送meet给新加入的节点,让新节点加入到集群中,然后新节点就会开始于其他节点进行通信
其实,内部就是发送了一个gossip meet信息,通知那个节点去加入我们的集群
fail
某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。
对比一下集中式与goosip协议
集中式
好处
元数据的更新和读取,时效性比较好
一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候要立即就可以感知到。
不好
所有元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
goosip协议
好处
元数据更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定延时,降低了压力
缺点
元数据更新有延时,可能会导致集群的一些操作会有一些滞后。
端口说明
说明
每个节点都有一个专门用于节点间通信的端口,就是自己的端口号+10000
每个节点间个一段时间都会往另外几个节点发送ping消息,同时其他节点收到ping之后返回pong
在Redis Cluster集群模式架构下,每个redis要开放两个端口
创建集群时,集群节点直接要相互通信,使用 redis-server的端口+10000 = 新的集群的监听端口
10000端口号
举例
6379
一个6379
16379
(6379+10000)
另外一个就要加10000的端口号,比如16379
16379端口号是用来进行节点间通信的
用来节点间通信的 gossip协议
也就是cluster bus的东西,集群总线。
cluster bus的通信,用来进行故障检测,配置更新,故障转移授权
cluster bus用了另外一种二进制协议,主要用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间
图解Redis Cluster集群模式的端口号
图解Redis Cluster集群模式的端口号
三类交换的信息
hash slot信息
故障信息
节点的增加删除
学习链接
集群方式总结
https://www.cnblogs.com/runnerjack/p/10269277.html
经典问题详解
http://www.luyixian.cn/news_show_319542.aspx
windows下多实例
https://www.jb51.net/article/96230.htm
其他
扩展性:迁移slot (同步)
dump
restore
remove
方案说明
每份数据分片会存储在多个互为主从的多节点上
数据写入先写主节点,再同步到到从节点
同一个分片多节点间的数据不保持一致性
读取数据时,当客户端操作的key没有分配在该节点时,redis会返回转向指令,指向正确的节点
扩容时需要把旧节点的数据迁移一部分到新的节点
请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上
集群写入数据
数据操作
客户端重定向
1、不使用重定向,则操作数据的key不属于当前客户端插槽德华,redis会报错提示前往对应的redis实例客户端操作数据;
2、使用重定向:redis-cli -c -p 6379
其中 -c 使命令自动重定向
其中 -c 使命令自动重定向
多键操作
1、不在一个slot下的键值,不能进行mget/mset等多键操作
2、通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到同一个slot中去
举例:mset {group}k1 v1 {group}k2 v2
读取数据
cluster keyslot key:计算key应该被放在哪个槽上
cluster countkeysinslot slot:返回槽slot目前包含的键值对数量->cluster countkeysinslot 12539
cluster getkeysinslot slot count:返回count个slot槽中的键
常见问题
主节点下线,从节点自动升为主节点
主节点恢复后,变为从节点
redis.conf中属性cluster-require-full-coverage控制当某一段插槽的主从节点都宕机以后,redis服务是否还继续。
高可用性与主备切换
redis cluster的高可用原理,几乎跟哨兵是类似的
1.判断节点宕机
如果一个节点认为另外一个节点宕机了,那么就是pfail,主观宕机
如果多个节点都认为另外一个节点宕机了,就是fail,客观宕机,跟哨兵几乎一样 sdown,odown
如果多个节点都认为另外一个节点宕机了,就是fail,客观宕机,跟哨兵几乎一样 sdown,odown
根据cluster-node-timeout内某个节点一直没有返回pong,那么就会被认为pfail
如果一个节点认为某个节点pfail了,那么就会在gossip消息中,ping给其他节点,如果超过半数节点都认为pfail了,那么就会变成fail
2.从节点过滤
对宕机的master node,从其所有slave node中,选择一个切换成master node
检查每个slave node与master node 的断开时间,如果超过了cluster-node-timeout*cluster-slave-validity-factory,那么久没有资格切换成master
3.从节点选举
每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举
所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(n/2+1)都投票给了某个从节点,那么选举通过,那个从节点可以成为master
从节点执行主备切换,从节点切换为主节点
扩容
准备新节点
加入集群
meet
redis-trib.rb add-node
迁移槽和数据
手工
1_对目标节点:cluster setslot {slot} importing {sourceNodeId}
2_对源节点:cluster setslot {slot} migrating {targetNodeId}
3_对源节点循环执行:cluster getkeysinslot {slot} {count},每次获取count个键
4_对源节点循环执行:migrate {targetIp} {targetPort} key 0 {timeout}
5_对所有主节点:cluster setslot {slot} node {targetNodeId}
pipeline migrate
redis-trib.rb reshard
故障转移
故障发现
通过ping/pong发现故障
主观下线
客观下线
当半数以上主节点都标记其为pfail
故障恢复
资格检查
准备选举时间
选举投票
替换主节点
伸缩
收缩
迁移槽
忘记节点
cluster forget {downNodeId}
redis-trib.rb del-node
关闭节点
迁移slot过程中如何同时提供服务?--> ask
0.先尝试访问源节点
1.源节点返回ASK转向
2.向新节点发送asking命令
3.向新节点发送命令
一致性: 保证朝着epoch值更大的信息收敛
重定向机制
如果当前实例没有这个键值对映射的哈希槽,那么实例返回MOVED包含新实例的访问地址
数据迁移过程中发送命令会收到ASK报错并包含正在迁移的新实例地址,
此时客户端需要先向新实例发送ASKING命令(请求执行命令),然后再发送GET命令
选举主节点
1.当某一个从节点发现自己的主节点已下线(状态为fail)时,从节点会广播一条消息,要求所有收到这条消息,并且具有投票权的主节点向这个从节点投票;
2.如果一个主节点具有投票权,并且这个主节点尚未给其他从节点投票,那么这个主节点会投票给该从节点;
3.如果集群中有N个主节点具有投票权,那么当一个从节点收集到N/2+1(超过半数)的投票时,这个从节点就成为新的主节点;
4.如果一个周期以内,没有收集到足够的支持票数,则集群进入下个配置周期,并在此发起选举,直到选举出新的主节点;
5.新的主节点出现后,撤销其哈希槽指派,并将其全部指派给自己;
6.对集群进行广播,告知其他节点已经成为新的主节点;
ps:从节点发现主节点下线时,不是立刻发起选举,而是会有一个配置时间+随机延迟,以减少多个从节点在同一时间发起选举的可能性;因为多个节点同时发起投票,会引发大家都没有获得半数以上的投票
扩展-保证在slot(槽位)迁移过程中的正常写入与修改
用个案例解答吧比较好理解:
假设从node A迁移槽 1 位到node B
迁移中会将 node A的这槽 1 状态标记为Migrating(迁移)
node B的状态为importing (输入口)
这时候槽的映射关系不做修改,迁移过程中如果出现查询槽 1 的数据,
客户端会先去node A上去查询数据,如果对应的数据还在本机上,那么直接返回,如果数据不在node A上 则会返回 ASK 转向。
如果客户端接收到 ASK 转向, 那么将命令请求的发送对象调整为转向所指定的节点node B。先发送一个 ASKING 命令,然后再发送真正的命令请求。
请求完成后不会更新客户端所记录的槽至节点的映射:槽 1 应该仍然映射到node A , 而不是node B 。
一旦node A 针对槽 1 的迁移工作完成, node A 在再次收到针对槽 1 的命令请求时, 就会向客户端返回 MOVED 转向, 将关于槽 1 的命令请求长期地转向到node B 。
假设从node A迁移槽 1 位到node B
迁移中会将 node A的这槽 1 状态标记为Migrating(迁移)
node B的状态为importing (输入口)
这时候槽的映射关系不做修改,迁移过程中如果出现查询槽 1 的数据,
客户端会先去node A上去查询数据,如果对应的数据还在本机上,那么直接返回,如果数据不在node A上 则会返回 ASK 转向。
如果客户端接收到 ASK 转向, 那么将命令请求的发送对象调整为转向所指定的节点node B。先发送一个 ASKING 命令,然后再发送真正的命令请求。
请求完成后不会更新客户端所记录的槽至节点的映射:槽 1 应该仍然映射到node A , 而不是node B 。
一旦node A 针对槽 1 的迁移工作完成, node A 在再次收到针对槽 1 的命令请求时, 就会向客户端返回 MOVED 转向, 将关于槽 1 的命令请求长期地转向到node B 。
客户端
客户端路由
moved
1.向任意节点发送命令
2.节点计算槽和对应节点
3.如果指向自身,则执行命令并返回结果
4.如果不指向自身,则回复-moved (moved slot ip:port)
5.客户端重定向发送命令
tips
redis-cli -c 会自动跳转到新节点
moved vs. ask
都是客户端重定向
moved: 表示slot确实不在当前节点(或已确定迁移)
ask: 表示slot在迁移中
批量操作
问题:mget/mset必须在同一个槽
实现
串行 mget
串行IO
客户端先做聚合,crc32 -> node,然后串行pipeline
并行IO
客户端先做聚合,然后并行pipeline
hash_tag
数据量大时fork子进程缓慢
运维
集群完整性
cluster-require-full-coverage=yes
要求16384个槽全部可用
节点故障或故障转移时会不可用:(error) CLUSTERDOWN
大多数业务无法容忍
带宽消耗
来源
消息发送频率
消息数据量
节点部署的机器规模
优化
避免“大”集群
cluster-node-timeout: 带宽和故障转移速度的均衡
尽量均匀分配到多机器上
集群状态下的pub/sub
publish在集群中每个节点广播,加重带宽
解决:单独用一套sentinel
倾斜
数据倾斜
节点和槽分配不均
redis-trib.rb info 查看节点、槽、键值分布
redis-trib.rb rebalance 重新均衡(慎用)
不同槽对应键值数量差异较大
CRC16一般比较均匀
可能存在hash_tag
cluster countkeyinslot {slot} 查看槽对应键值数
包含bigkey
从节点执行 redis-cli --bigkeys
优化数据结构
内存配置不一致
请求倾斜
原因:热点key、bigkey
优化
避免bigkey
热键不要用hash_tag
一致性不高时,可用本地缓存,MQ
热点key解决思路
客户端统计
实现简单
内存泄露隐患,只能统计单个客户端
代理统计
增加代理端开发部署成本
服务端统计(monitor)
monitor本身问题,只能短时间使用
只能统计单个redis节点
机器段统计(抓取tcp)
无侵入
增加了机器部署成本
读写分离
只读连接
从节点不接受任何读写请求
会重定向到负责槽的主节点(moved)
readonly命令
读写分离客户端会非常复杂
共性问题:复制延迟、过期数据、从节点故障
cluster slaves {nodeId} 获取从节点列表
数据迁移
redis-trib.rb import
只能 单机 to 集群
不支持在线迁移
不支持断点续传
单线程迁移,影响速度
在线迁移
唯品会 redis-migrate-tool
豌豆荚 redis-port
Codis
Codis 是一个分布式 Redis 解决方案
用zookeeper存储槽位关系
代价
不支持事务
rename 操作也很危险
为了支持扩容,单个 key 对应的 value 不宜过大
网络开销更大
需要运维zk
代理proxy
缓存与数据库一致性
先写缓存,在写数据库,缓存成功,数据库失败(脏读)
Redis架构如何才能做到99.99%高可用 在master故障时,自动检测,并将某个slave自动切换为master node的过程叫主备切换。这个过程,实现了redis的主从架构下的高可用性。 一旦master故障,在很短时间内,就会切换到另一个master上去。
高可用性:如果你的系统可以保证在全年,99.99%的时间内,都是处于可用的状态的,那么就可以称之为高可用性。
redis宕机出现的问题
如果一个slave挂掉了,是不会影响可用性的,还有其他slave提供相同数据下的相同的对外查询服务
但是如果master挂掉了,写缓存的时候全部失效了,slave也没有主节点给他们复制数据了,系统相当于不可用了
缓存不可用,高并发高性能的缓存不可用了,大量的流量,超过mysql承载范围内的大并发会涌入mysql,mysql宕机
缓存不可用,高并发高性能的缓存不可用了,大量的流量,超过mysql承载范围内的大并发会涌入mysql,mysql宕机
redis怎么做到高可用?->redis高可用架构,叫做故障转移,failover,也可以叫做主备切换
如何通过读写分离承载QPS超过10万+
读写分离
写 master
读 slave
架构做成主从架构一主多从,主负责写,并将数据同步复制到其他的slave节点,从节点负责读。所有读请求全部走丛节点
Redis高并发
主从架构,一主多从
一般来说很多项目就足够了
单主用来写入数据,单机几万qps
多从用来查询数据,多个实例可以提供每秒10万qps
redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如redis主就10G内存量,其实你就只能容纳10G的数据量。
但如果你的缓存要容纳的数据量很大,达到几十G甚至几百G几T,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒几十万的读写并发。
Redis的安装与部署
不同操作环境下安装Redis、Redis安装的基本步骤
windows下安装(不推荐)
1、下载安装包
Redis官网下载页面: http://redis.io/download
Windows下Redis项目: https://github.com/MSOpenTech/redis
从github上查找redis下载
在github上搜索Redis,
2、下载获得压缩包,解压到自己电脑目录下就可以
下载zip压缩包,解压即可
解压后的文件说明
1.redis-benchmark.exe #基准测试
2.redis-check-aof.exe # aof
3.redis-check-dump.exe # dump
4.redis-cli.exe # 客户端
5.redis-server.exe # 服务器
6.redis.windows.conf # 配置文件
找到解压后的文件,图解解压后的文件说明
大概就是这样
3、开启redis,运行服务server即可
4、使用redis客户端Client,连接redis(服务server不能关)
双击Redis.server.exe打开Redis
这个终端窗口不要关闭,不然服务就会被终止
5、测试连接ping,返回‘PONG’就是成功
双击Redis-cl.exei测试
测试是否连接成功
测试存取值
6、设置值set name value
单实例运行
手动运行
redis-server.exe redis.windows.conf
服务运行
安装redis服务
redis-server --service-install redis.windows.conf --loglevel verbose命令注册服务
启动redis服务
redis-server --service-start开启服务
卸载redis服务
redis-server.exe --service-uninstall
客户端运行
redis-cli.exe 默认访问6379端口
redis-cli.exe -p 6380 访问指定的端口6380
Linux下安装(官方推荐)
官方推荐使用Linux部署
linux安装Redis(单机)
redis推荐在linux上搭建使用,不建议在windows上搭建
1、下载安装包,直接从官网下载,首页‘Download it’,获得安装包
下载安装包(Redis.io)
2、使用XShell连接服务器,使用Xftp将安装包上传
3、解压redis的安装包,放入/opt目录下
解压压缩包
发送到服务器的opt(软件一般都安装在这里)目录下
4、进入到redis文件中,可以看到redis.conf配置文件
5、使用前的操作:安装gcc环境 与 软件编译
安装gcc-c++
编译需要环境支持,如果没有c++环境的先配置环境
安装gcc环境(C语言编译环境)
进本环境安装
yum install gcc-c++
查看
gcc -v
进入Redis文件夹进行软件编译
输入make
将所有需要配置文件进行配置
输入make install
确认是否所有都安装好
6、redis默认安装路径
/usr/local/bin
Other
打开阿里云把文件copy到software里面
cd /usr/local
mkdir software
tar -zxvf xxx.tar.gz
mkdir software
tar -zxvf xxx.tar.gz
7、可以在bin路径下找到server、client,操作如同window一样
然后我们会在/usr/local/bin下发现我们已经安装好的Redis
8、可将bin目录下文件redis.conf拷贝到当前目录下
然后我们在/usr/local/bin下建立一个专门存放Redis配置文件的文件夹,因为Redis之后组集群会有很多配置文件
就像这样
9、redis默认不是后台启动的,修改配置文件
进入redis.conf中,将daemonize no 改成yes
修改Redis的配置文件,改成后台运行
大概在11%的位置
10、启动redis 服务
redis-server kconfig/redis.conf
通过指定的配置文件启动服务
11、启动客户端
redis-cli -p 6379
12、测试
# 连接是否成
ping
# 设置值
set name
# 获取值
get name
# 查看所有值
keys *
ping
# 设置值
set name
# 获取值
get name
# 查看所有值
keys *
同Windows下安装一样
13、查看redis进程是否存在
命令:ps -ef|grep redis
14、如何关闭redis服务呢?
命令:shutdown
关闭并退出
注意事项:外部访问记得打开防火墙
Redis线上生产环境时如何部署的?
redis cluster,部署了10台机器,5台机器部署了redis的主实例,另外5台机器部署了redis从实例,每个主实例挂了一个从实例,5个节点提供读写服务,每个节点的读写高峰可能可以达到每秒5万,5台机器最多时25万读写请求/s。
机器配置:32G内存+8核CPU。但是分配给redis进程的是10G内存,一般线上生产环境,redis的内存尽量不要超过10G,超过10g可能会有问题。
5台机器对外提供读写,一共50G内存。
5台机器对外提供读写,一共50G内存。
因为每个主实例都挂了一个从实例,所以时高可用的,任何一个主实例宕机,自动故障迁移,redis从实例会自动变成主实例继续提供读写服务。
往内存里写了什么数据?每条数据的大小是多少?
保险数据,每条数据10Kb,100条是1mb,10万条数据是1g。常驻内存的是200万条保单数据,占用内存是20g,仅仅不到总内存的50%
目前高峰期每秒就是3500左右的请求量
Redis安装、配置、启动
官网
安装
安装Redis
安装gcc环境(C语言编译环境)
配置
单位说明:1k(1000bytes)!=1kb(1024bytes)
include:引入公共配置文件
network
bind:限定访问的主机地址
protected-mode:是否开启安全防护模式
port:指定redis实例端口号,默认6379
tcp-backlog:指定高并发时访问排队的长度,超过后就阻塞。
timeout:超时时间
tcp-keepalive:对客户端的心跳检测间隔时间
general
daemonize:是否开启守护线程模式运行
pidfile:指定进程id文件保存的路径
loglevel:指定日志级别
debug
verbose
notice
warning
logfile:指定日志文件路径
syslog-enabled:是否记录到系统日志
syslog-ident:设置系统日志的id
syslog-facility:指定系统日志设置,必须为user或local0-local7之间的值
databases:设置数据库数量,默认为16(0-15)
其他
requirepass:设定访问密码
maxclients:最大连接数
maxmemory:最大占用多少内存
maxmemory-policy:缓存清理策略
volatile-lru
allkeys-lru
volatile-random
allkeys-random
volatile-ttl
noeviction
启动
Redis.conf配置详解
进入方式
进入服务器找到redis安装的目录,找到redis.conf 文件进行配置
单位说明
1k(1000bytes)!=1kb(1024bytes)
配置文件unit单位 对大小写不敏感
单位
网络Network
bind
bind 127.0.0.1
限定访问的主机地址
绑定的ip
protected-mode
是否开启安全防护模式
保护模式
protected-mode yes
port
端口设置
指定redis实例端口号
默认6379
port 6379
tcp-backlog
指定高并发时访问排队的长度,超过后就阻塞
timeout
超时时间
tcp-keepalive
对客户端的心跳检测间隔时间
通用General
daemonize
是否开启守护线程模式运行
daeminize yes
以守护进程的方式运行,默认是no,需要自行开启为yes
pidfile
指定进程id文件保存的路径
pidfile /var/run/redis_6379.pid
如果以后台的方式运行,就需要指定一个 pid
loglevel
debug
verbose
notice
warning
logfile
logfile ""
指定日志文件路径
日志的文件位置名
databases
设置数据库数量
默认为16(0-15)
databases 16
数据库的数量,默认是 16 个数据库
always-show-log
是否总是显示log
always-show-log yes
默认为yes
syslog-enabled
是否记录到系统日志
syslog-ident
设置系统日志的id
syslog-facility
指定系统日志设置,必须为user或local0-local7之间的值
其他
include:引入公共配置文件
requirepass:设定访问密码
maxclients:最大连接数
maxmemory:最大占用多少内存
maxmemory-policy:缓存清理策略
volatile-lru
allkeys-lru
volatile-random
allkeys-random
volatile-ttl
noeviction
安全Security
设置密码,这是通过在配置文件中设置的
命令设置密码
config get requirepass
config set requirepass "123456"(password)
auth加权限
config get requirepass
config set requirepass "123456"(password)
auth加权限
限制Clients,内存设置
maxclients 10000
设置能连接上的redis的最大客户端数量
maxmemory <bytes>
redis 配置最大的内存容量
maxmemory-policy noeviction
#内存到达上限后的处理策略
处理策略
持久化相关配置
RDB配置
持久化,在规定的时间内,执行了多少次操作,则会持久化到文件.rdb .aof
redis是内存数据库,如果没有持久化,那么断电数据既失
# 如果 900s 内,如果至少有一个1 key进行了修改,我们及进行持久化操作
save 900 1
# 如果 300s 内,如果至少有一个10 key进行了修改,就会进行持久化
save 300 10
# 如果 60s 内,如果至少有一个10000 key进行了修改,就会进行持久化
save 60 10000
save 900 1
# 如果 300s 内,如果至少有一个10 key进行了修改,就会进行持久化
save 300 10
# 如果 60s 内,如果至少有一个10000 key进行了修改,就会进行持久化
save 60 10000
stop-writes-on-bgsave-error yes # 持久化如果出错,是否还需要继续工作
rdbcompression yes #是否压缩redb文件,需要消耗一些cpu资源
rdbchecksum yes # 保存rdb文件的时候,进行错误的检查校验
dir ./ # rdb文件保存的目录
AOF配置
appendonly on # 默认是不开启aof模式的,默认是使用rdb方式持久化的,大部分所有的情况下,rdb够用了
appendfilename “appendonly.aof” 持久化文件的名字
基本配置
Java整合Redis客户端
Java整合Redis(Lua脚本)
Lua脚本
1、常用作嵌入式脚本语言
2、将复杂的redis操作,写为一个脚本,交于redis执行,减少连接redis的次数
3、具有一定的原子性,不被其他命令插队,可以完成事务操作
4、redis2.6以上的版本才可以使用
在Redis中调用lua脚本
redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
在lua脚本中调用redis命令
redis> eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value
案例:对IP进行限流
缓存lua脚本
lua脚本超时
SpringBoot整合Redis(Jedis、Lettuce)
Spring Boot 可以非常方便地集成 Redis
Jedis
什么是Jedis、简介
Redis Client
通过Java连接Redis
使用java操作Redis中间件!
是redis官方推荐的java连接开发工具!
原生redis命令,springboot3 已弃用
如果要使用java操作redis,那么一定要对Jedis十分熟悉
通过Java连接Redis
Jedis: 一款java操作redis数据库的工具
项目使用
新建项目,引入依赖
导入jedis依赖包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
0. 导入jar包
commons-pool2-2.3.jar
jedis-2.7.0.jar
导入fastjson依赖包,阿里巴巴的
配置一下远程Redis
开启后台运行
开启保护模式
设置密码
重启Redis服务
打开redis-cli连接本地服务,需要输入auth 密码才能继续后续操作,不然没有权限
然后输入shutdown关闭服务
redis-server redisconfigs/redis.config打开服务
编码测试
连接数据库
Jedis jedis = new Jedis(ip,port);
jedis就可以使用所有指令方法
操作命令
基础
断开连接
编写测试代码
package com.xiaoye;
import redis.clients.jedis.Jedis;
/**
* @author 小也
* @create 2021/4/28 19:47
*/
public class Test {
public static void main(String[] args) {
//万物皆对象,先弄个jedis的对象出来,构造函数为主机地址和端口号
Jedis jedis = new Jedis("8.140.12.96",6379);
//给密码,获取权限
jedis.auth("密码");
//测试是否成功连接,控制台输出PONG则连接成功
System.out.println(jedis.ping());
}
}
使用步骤
1. 获取连接
Jedis jedis = new Jedis("localhost",6379);
2. 操作
jedis.set("username","zhangsan");
3. 关闭连接
jedis.close();
核心接口
Jedis
JedisPoolConfig
JedisPool
配置
常规配置
连接池配置
核心方法
添加数据
jedis.set();
获取数据
jedis.get()
修改数据
jedis.append();
覆盖原有数据
jedis.set();
删除数据
jedis.del();
key-value 数组存储
jedis.msetnx();
数组提取
jedis.mget()
map数据
jedis.hmset();
jedis.hmget();
jedis.hlen()
jedis.hexists()
jedis.hkeys()
jedis.hvals()
连接池:JedisPool
1.创建配置文件对象
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50);
config.setMaxIdle(10);
.......
config.setMaxTotal(50);
config.setMaxIdle(10);
.......
1. 创建JedisPool连接池对象并加载配置文件
JedisPool jedisPool = new JedisPool(config,"localhost",6379);
2. 调用方法 getResource()方法获取Jedis连接
Jedis jedis = jedisPool.getResource();
3. 操作
4. 关闭 归还到连接池中
连接池工具类:JedisPoolUtils
private static JedisPool jedisPool;
static{
//读取配置文件
InputStream is = JedisPoolUtils.class.getClassLoader().getResourceAsStream("jedis.properties");
//创建Properties对象
Properties pro = new Properties();
//关联文件
try {
pro.load(is);
} catch (IOException e) {
e.printStackTrace();
}
//获取数据,设置到JedisPoolConfig中
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(Integer.parseInt(pro.getProperty("maxTotal")));
config.setMaxIdle(Integer.parseInt(pro.getProperty("maxIdle")));
//初始化JedisPool
jedisPool = new JedisPool(config,pro.getProperty("host"),Integer.parseInt(pro.getProperty("port")));
}
/**
* 获取连接方法
*/
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
static{
//读取配置文件
InputStream is = JedisPoolUtils.class.getClassLoader().getResourceAsStream("jedis.properties");
//创建Properties对象
Properties pro = new Properties();
//关联文件
try {
pro.load(is);
} catch (IOException e) {
e.printStackTrace();
}
//获取数据,设置到JedisPoolConfig中
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(Integer.parseInt(pro.getProperty("maxTotal")));
config.setMaxIdle(Integer.parseInt(pro.getProperty("maxIdle")));
//初始化JedisPool
jedisPool = new JedisPool(config,pro.getProperty("host"),Integer.parseInt(pro.getProperty("port")));
}
/**
* 获取连接方法
*/
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
单机开发
单机开发
集群开发
集群开发
面向集群的jedis内部实现原理
开发,jedis,redis和java 客户端, redis cluster,jedis cluster api
基于重定向的客户端
redis-cli -c 自动重定向
请求重定向
- 客户端可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会根据key计算对应的hash slot
- 如果在本地就在本地处理,否则返回moved给客户端,客户端进行重定向
- cluster keyslot mykey,可以查看一个key对应的hash slot是什么
- 用redis-cli的时候,可以加-c参数,支持自动的请求重定向,redis-cli接收到moved之后,会自动重定向到对应的节点执行命令。
计算hash slot
就是根据key计算CRC16的值,然后跟苦16384取模,拿到对应的hash slot
用hash tag可以手动指定key对应的slot,同一个hash tag下的key,都会在一个hash slot中,比如set mykey1:{100}和setmykey2:{100}
用hash tag可以手动指定key对应的slot,同一个hash tag下的key,都会在一个hash slot中,比如set mykey1:{100}和setmykey2:{100}
hash slot 查找
节点间通过gossip协议进行数据交换,就知道每一个hash slot在哪个节点上。
smart jedis
基于重定向的客户端,很消耗IO,因为大部分情况下,可能都会出现一次重定向,才能找到正确的节点
所以大部分客户端,比如jedis,都是smart的
本地维护一份hash slot->node的映射表缓存,大部分情况下,直接走本地缓存就可以找到hashslot ->node ,不需要通过节点进行moved重定向
所以大部分客户端,比如jedis,都是smart的
本地维护一份hash slot->node的映射表缓存,大部分情况下,直接走本地缓存就可以找到hashslot ->node ,不需要通过节点进行moved重定向
JedisCluster工作原理
- 在JedisCluster初始化的时候,就会随机选择一个node,初始化hashslot->node映射表,同时为每个节点创建一个jedisPool连接池
- 每次基于JedisCluster执行操作,首先会在本地计算key的hashslot,然后在本地缓存的映射表找到对应的节点。
- 如果node正好还持有hash slot就ok;如果说进行了reshard这样的操作,hashslot不在那个节点上了,就会返回moved
- 然后JedisCluster发现对应节点返回moved,那么利用该节点的元数据,更新本地的hash slot ->node的映射表缓存
- 重复上面步骤,知道找到对应节点,如果重试超过5次,那么就报错,JedisClusterMaxRedirectionException。
hashslot迁移和ask重定向
如果hash slot正在迁移,那么会返回ask给jedis
jedis接到ask重定向之后,会重定向到目标节点去执行,但是因为ask发生在hash slot迁移过程中,所以不会更新本地缓存的映射表
lettuce
springboot2.x就把底层的redis操作从jedis改成了lettuce,因为jedis是线程不安全的
通过 redisTemplate.getConnectionFactory().getConnection(); 获得connection对象操作
Redisson
本质
特点
实现分布式锁
命令更抽象,让程序员专注于业务逻辑
支持的数据类型更多
注意
springboot 2.x之后默认使用lettuce,若使用jedis配置,因为源码中许多类没有注入,类不存在,因此别使用jedis,有可能导致配置不成功
jedis 不安全,需要使用 pool 池化,像BIO
lettuce 安全,像NIO,现在比较常用
使用
redisTemlpate.opsForXXX() opsFor->operation for 操作
除了基本的操作,常用的方法也可以直接使用 redisTemplate 操作,比如事务
获取连接 redisTemplate.getConnectionFactory().getConnection()
connection.flushDB()
connection.flushAll()
序列化
序列化配置,JDK序列化
开发中需要序列化,否则存入后取值会报错
序列化方式
实现Serialized接口
对应的实现的序列化
使用最好自己封装好工具包util
自己开发方便,一般直接使用String
定义后,可以使用@Qualifer("reidsTemplate")
Redis与Spring的集成
方向1:解决Session共享问题
方向2:redis作为数据库使用
NoSql
Redis线程模型
Redis是单线程还是多线程
基于Reactor模式开发了网络事件处理器
redis是基于reactor模式开发了网络事件处理器,也叫文件事件处理器。
这个文件事件处理器是单线程的,所以redis也被叫做单线程的模型。
原理
是通过采取IO多路复用机制同时监听几个socket,根据socket的时间来选择对应的事件处理器。
socket有accept,read,write,close等事件
通过上面的形式实现了高性能的通信模型【NIO】
文件事件处理器
文件事件处理器是单线程的,所以redis才叫单线程的模型
流程
1.客户端连接到server sorket请求建立连接
2.serversorket接收到请求,产生一个AE_READABLE事件
3.redis内部有一个IO多路复用程序,监听所有的socket产生的事件,将事件压倒队列里
4.文件事件分派器会从队列中拿到对应的socket和事件,然后根据socket产生的事件选择事件处理器
比如文件分派处理器对AE_READABLE事件给到连接应答处理器处理这个事件
比如文件分派处理器对AE_READABLE事件给到连接应答处理器处理这个事件
三个事件处理器
连接应答处理器
命令请求处理器
命令回复处理器
5.连接应答处理器会跟客户端建立对应连接,建立相应的socket01连接客户端,然后把socket01的AE_READABLE事件和命令请求处理器关联。
6.客户端发送set请求,socket01接收到会产生AE_READABLE事件,然后被IO多路复用程序监听到产生的事件,将事件压倒队列中
7.之前连接应答处理器已经将socket01上的AE_READABLE事件跟命令请求处理器关联了,所以这个时候文件事件分派器会直接找命令请求处理器处理socket01上面新的AE_READABLE事件。
8.命令请求处理器会把socket01里的读出来请求的相关数据,进行执行和处理。
(从socket01中读取出来key和value,然后在自己内存中完成key和value的设置。)
命令请求处理器将socket01的AE_WRITABLE事件跟命令回复处理器关联起来。
(从socket01中读取出来key和value,然后在自己内存中完成key和value的设置。)
命令请求处理器将socket01的AE_WRITABLE事件跟命令回复处理器关联起来。
9.当客户端那边准备好读取响应数据时,就会在socket01上产生一个AE_WRITABLE事件,然后socket再被IO多路复用器放到队列。
由于AE_WRITABLE已经与命令回复处理器关联起来了,队列中的AE_WRITABLE就会找命令回复处理器。
命令回复处理器对socket01输出本次操作的一个结果:OK
由于AE_WRITABLE已经与命令回复处理器关联起来了,队列中的AE_WRITABLE就会找命令回复处理器。
命令回复处理器对socket01输出本次操作的一个结果:OK
10.客户端就会收到set请求的返回结果:OK
11.socket01的AE_WRITABLE事件跟命令回复处理器解除关联。
使用Redis需要解决的问题
缓存更新
缓存更新除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),还可以根据具体的业务需求进行自定义的缓存淘汰
常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
缓存异常
缓存穿透
缓存中某一热点数据失效,同一时间大量请求直接到了数据库
设置合理的过期时间(永不过期)
缓存穿透
描述:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决:接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
布隆过滤器
记忆:关键字“透”,是指透过了缓存、数据库
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。
缓存雪崩
描述
是指在某一个时间段,缓存集体过期失效或者redis宕机
缓存雪崩(缓存时间过期失效)
缓存雪崩(某一时刻大批量k/v失效)
指的是缓存大面积在同一时间失效,导致数据库短时间内无法处理大量请求,导致数据库崩溃
缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
大面积的缓存失效,大量请求直接请求数据库,打崩了数据库
图解
图解
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
原有的过期时间加一个随机值,这样重复率就少了
原有的过期时间加一个随机值,这样重复率就少了
出现场景
假设每天高峰期每秒请求5000次,缓存可以抗每秒4000次,剩下1000个请求落到数据库上。
缓存redis挂了,5000个请求直接请求到数据库,此时数据库直接崩溃。系统全死了
集中过期倒不是最致命的,最终名的雪崩,是缓存服务器某个节点宕机或断网,自然形成的雪崩;
因为是缓存服务器节点宕机,对数据库造成压力是不可以预知的,谁也不知道下一秒是不是就会导致数据库压垮
因为是缓存服务器节点宕机,对数据库造成压力是不可以预知的,谁也不知道下一秒是不是就会导致数据库压垮
使用场景:双十一期间,为了保证高效,可能为关闭当天退款的功能,等第二天或者等其他时候可以退款
淘宝双十一抢购,假设商品集体缓存过期,导致缓存查不到,压力全部压到数据库上
解决方案、解决办法
热点数据不过期
定时刷新
将过期时间离散化-随机过期时间
增加互斥锁更新
服务本地做二级缓存
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
加锁排队
给每个缓存数据增加缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新缓存
批量往数据库存数据的时候,把每个key的失效时间都加上一个随机值,这个可以保证数据不会在同一时间大面积失效
热点数据永不过期,有更新时更新一下缓存
redis高可用
既然redis可能挂掉,那么就多增设几台redis,这样一台挂了,其他还可以工作,其实就搭建的集群
限流降级
在缓存失效后,通过加锁或者队列来控制读数据数据库写缓存的线程数量,比如对某个key只允许一个线程查询数据和写数据,其他线程等待
先停掉一些服务,保证其他服务可用
数据预热
数据加热的含义:在正式部署之前,先把可能的数据选预先访问一边,这样部分可能大量访问的数据就会加载到缓存中。
在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
解决
事前
缓存必须高可用,主从+哨兵/redis cluster,避免全盘崩溃
最好不要把缓存只放在redis里面,可以在系统内部用ehcache在维护一个缓存
1.用户发送一个请求
2.收到请求,先查本地ehcache缓存,如果没有再查redis
3.如果ehcache和redis都没有,就查数据库
4.将数据库中的结果,写入ehcache和redis中
事中
本地ehcache缓存+hystrix限流&降级,避免mysql被打死
可以设置每秒2000个请求,一秒过来5000个请求,此时只会有2000个请求会通过限流组件,进入数据库
剩下3000个走降级,返回一些友好提示。
剩下3000个走降级,返回一些友好提示。
好处:
- 1.数据库绝对不会死,限流确保了每秒只会过去2000个请求。
- 2.数据库不死,对用户来说2/5是可以被处理的
- 3.只要有2/5请求可以被处理,就意味着你的系统没死,多点几次可能还能用,用户感觉只是有点故障。
事后
redis一定要做持久化,尽快恢复缓存集群,一旦重启,自动从磁盘上加载数据,恢复内存中的数据
描述
解决方案
描述:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
设置热点数据永远不过期。
什么是缓存雪崩
缓存雪崩解决方案
缓存雪崩
描述
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。
和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
一般有三种处理办法:
1. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
2. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
3. 为key设置不同的缓存失效时间。
2. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
3. 为key设置不同的缓存失效时间。
解决
加随机值
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
集群部署
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
设置热点数据永远不过期。
缓存雪崩优化
什么是缓存雪崩
由于缓存存放者大量的请求,当缓存服务异常或者脱机,流量直接压向后端服务器,造成级连故障
解决方法
1、保证缓存的高可用
1、Redis集群
2、Redis-Sentinel
子主题
2、依赖隔离组件进行限流
3、提前演练(压力测试等)
缓存雪崩
缓存雪崩
同一时间大面积缓存失效
事前保证集群高可用
本地ehcache缓存+hystrix限流降级,通过加锁或队列空值写操作的线程数量
将不同数据的缓存时间设置的分散些
事后持久化机制,尽快恢复数据
缓存穿透
描述
缓存穿透(访问的大批量key都不在缓存中)
缓存设置默认值
短暂缓存空值
布隆过滤器
无效key业务上校验过滤
缓存穿透(库都没数据)
指缓存和数据库中都没有数据,导致所有的请求都落到数据库上,崩溃
用户不断地发起缓存和数据库都不存在的数据,导致数据库压力过大,严重可击垮数据库
用户想要查一个数据,发现redis内存数据库没有(也就是缓存没有命中),于是向持久层数据库查询(类似mysql)。
发现持久层数据也没有,于是本次查询失败。
当用户很多的时候,缓存都没有命中(举例场景:秒杀活动),于是都去请求持久层数据库,最终持久层数据库带来压力,就相当于缓存穿透
发现持久层数据也没有,于是本次查询失败。
当用户很多的时候,缓存都没有命中(举例场景:秒杀活动),于是都去请求持久层数据库,最终持久层数据库带来压力,就相当于缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案:布隆过滤器或者对空进行缓存,过期时间设置5分钟
解决方案:布隆过滤器或者对空进行缓存,过期时间设置5分钟
解决方案
布隆过滤器
把请求存起来,存空
例子
恶意攻击
现象
请求redis不存在的缓存数据,导致请求全部打到mysql数据库,形成缓存穿透
出现场景
秒杀活动
解决方案、解决办法
接口层增加校验,如用户鉴权校验,例如id<0直接拦截
从缓存获取不到数据,数据库也找不到,可以设置key -null的缓存,并设置一个比较短的过期时间
采用布隆过滤器,将所有可能存在的数据存储到一个bitmap中,不存在的就提前过滤掉
增加参数校验
布隆过滤器(Bloom Filter)
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免对底层存储系统的查询压力
缓存空对象
当存储层不命中,及时返回空对象也将其缓存起来,同时会设置一个过期时间,之后在访问这个数据将会从缓存中获取,保护了后端数据源
可能存在两个问题
1、如果控制能够被缓存,这就意味着缓存需要更多的空间存储来存储更多的键
2、及时对空值设置了过期时间,还是会存在缓存层和存储层的数据,会出现一段时间窗口不一致,这对保持一致性的业务有影响
情景:
一秒5000个请求,结果其中4000个请求是黑客发出的恶意攻击
5000个请求只有1000个请求在redis中能查到,4000个请求都得去数据库里查,但是查不到,下次又去数据库里查
解决
每次系统A从数据库只要没查到,就写一个空值到缓存里 set -999 UNKNOWN
描述:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决:接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
记忆:关键字“透”,是指透过了缓存、数据库
详见上文中的"缓存穿透"
布隆过滤器问题:
1.只能增加,不能减少;
比如:redis中原来存在某一个key,但是后来去除了,布隆过滤器中不好减少
布隆过滤器问题:
1.只能增加,不能减少;
比如:redis中原来存在某一个key,但是后来去除了,布隆过滤器中不好减少
什么是缓存穿透
哈希碰撞
布隆过滤器原理
Guava实现
布隆过滤器在项目中使用
缓存的穿透优化
缓存穿透的现象
大量请求访问缓存,缓存找不到,
原因
1、代码问题
2、恶意访问或者爬虫等
如何发现
1、业务的响应时间
2、业务本身问题
3、相关指标
1、总调用数
2、缓冲层命中数
3、存储层命中数
解决方法
1、缓存空对象(同时设置过期时间)
可能的问题
1、产生更多的键
2、缓存层和存储层数据短期不一致
2、步隆过滤器拦截
缓存击穿
描述
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决
设置热点数据永不过期
热点数据不失效
加互斥锁
互斥锁
缓存击穿
描述:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决
设置热点数据永不过期
加互斥锁
描述
缓存击穿(读缓存又读库)
缓存击穿指并发查同一条数据
缓存击穿 (单个热点数据过期 大量请求到db)
指缓存中没有,数据库中有,并发用户大,同时读取缓存没读取到数据,又同时读取数据库,引起数据库压力大
一个key非常热点,高并发集中对这个点进行访问,当这个key失效的一瞬间,大量的请求直接请求数据库,击垮数据库,就像在完好无损的桶上凿开了一个洞
是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般就是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库瞬间压力过大!
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般就是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导致数据库瞬间压力过大!
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
1)采取限流组件,然后异步的吧一些即将过期的热点数据再重新构建
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
1)采取限流组件,然后异步的吧一些即将过期的热点数据再重新构建
出现场景
微博热搜
微博热搜宕机
现象
某个key非常热点,承担了超高并发,当这个key失效,持续的超高并发请求就会达到mysql数据库上
解决方案、解决办法
设置热点数据永不过期
热点数据不过期
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题
存在问题:需要更多空间存储
增加互斥锁
增加互斥锁更新
加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务器,其他线程没有获得分布式锁的权限,因此需要等待即可。
这种方式将高并发的压力转移到分布式锁,因此对分布式 锁的考验很大
这种方式将高并发的压力转移到分布式锁,因此对分布式 锁的考验很大
定时刷新
业务逻辑检查更新
描述
一个存在的key,在缓存过期之后,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。
注意点:
+ 1.一个key
+ 2.大量并发
+ 3.击穿到DB
注意点:
+ 1.一个key
+ 2.大量并发
+ 3.击穿到DB
解决方案
现象
大量请求在缓存中失效,全部打进mysql数据库
例子
缓存时间过期,或者redis宕机
解决方案
多设置几台redis服务器
限流降级,保证服务可用
设置缓存时间随机,失效均匀
缓存击穿和缓存穿透区别
击穿 -> 量大!key存在,但缓存过期!(热点)
穿透 -> key不存在
缓存击穿
仅仅只针对某一个key(可以是热点key重构)
缓存击穿
访问数据库中不存在的数据,会不断访问数据库
缓存空对象
访问数据库结果为不存在时,将返回空值,保存到缓存中,设置过期时间。
缺点:空对象浪费空间;缓存层和存储层存在一致性问题
缺点:空对象浪费空间;缓存层和存储层存在一致性问题
加过滤器
预先将所有可能存在的数据存到BitMap,用布隆过滤器进行校验,一定不存在的会被拦截
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
缓存预热
描述
系统上线后,将相关的缓存数据直接加载到缓存系统中
解决方案
直接写个缓存刷新页面,上线时手工操作下
数据量不大,可以在项目启动的时候自动加载
定时刷新缓存
布隆过滤器(Bloom Filter)
命令
bf.add
bf.exists
bf.madd
bf.mexists
描述
可以用于检索一个元素是否在一个集合中
应用场景
爬虫过滤已抓到的url就不再抓
邮件系统的垃圾邮件过滤功能
新闻客户端推荐系统去重
特性
有一定的误差
当布隆过滤器说某个值存在时,这个值可能不存在;当它说某个值不存在时,那就肯定不存在
缓存穿透/Bloom布隆过滤器
脑裂问题
描述:由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致大量数据丢失。
解决:min-replicas-to-write 3
min-replicas-max-lag 10
第一个参数表示最少的salve节点为3个,第二个参数表示数据复制和同步的延迟不能超过10秒
配置了这两个参数:如果发生脑裂:原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。
min-replicas-max-lag 10
第一个参数表示最少的salve节点为3个,第二个参数表示数据复制和同步的延迟不能超过10秒
配置了这两个参数:如果发生脑裂:原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。
异步复制,脑裂数据丢失
异步复制数据丢失
数据写入master,master还没异步发送给slave就挂掉了,sentinel选举新的master后导致这些还没来得及异步发送的数据就丢失了
解决:两个参数
min-slaves-to-write 1
min-slaves-max-lag 10
至少有一个slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候master就不会再接收任何请求
client去做一些其他处理,比如
1.在client做降级,写到本地磁盘里面,在client对外接收请求,再做降级,做限流,减慢请求涌入速度。
2.client可能会采取将这个数据临时灌入一个kafka消息队列,每隔10分钟去队列里面取一次,尝试重新发挥master。
min-slaves-to-write 1
min-slaves-max-lag 10
至少有一个slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候master就不会再接收任何请求
client去做一些其他处理,比如
1.在client做降级,写到本地磁盘里面,在client对外接收请求,再做降级,做限流,减慢请求涌入速度。
2.client可能会采取将这个数据临时灌入一个kafka消息队列,每隔10分钟去队列里面取一次,尝试重新发挥master。
脑裂导致数据丢失
master节点与其他节点丢失连接,其他节点选举新的master。
但是原来的master可能和客户端正常连接,客户端在脑裂时还在往原来的master里写数据。
手动解决脑裂问题,把原来的master关闭设置为slave节点,导致脑裂时客户端往原来master里发的一部分数据丢失。
但是原来的master可能和客户端正常连接,客户端在脑裂时还在往原来的master里写数据。
手动解决脑裂问题,把原来的master关闭设置为slave节点,导致脑裂时客户端往原来master里发的一部分数据丢失。
解决:通上两个参数
控制1个slave已经10s没有跟master做数据同步,就不接受client消息。
master连接不到其他slave,控制不让client再往它里写数据。
控制1个slave已经10s没有跟master做数据同步,就不接受client消息。
master连接不到其他slave,控制不让client再往它里写数据。
Redis和DB数据一致性
双写一致性
延时双删
数据一致性
缓存使用场景
一致性问题定义
方案选择
数据一致性怎么保证
先删缓存,在更新数据库,可能会脏读
先更新,成功之后删除缓存
更新缓存,然后通过异步调度来批量更新数据库,性能好但是无法保证强一致性
一致性hash
一致性hash出现的由来:
由于数据的不断增加,单台redis内存已经不能满足实际需求,我们需要将数据分区。
+ 简单的hash取模分区:
通过散列函数将key变成固定长度数字,对其取模,取模结果对应第几台redis实例,但是这种方式不利于redis的扩展及删除,有局限性。
+ 一致性hash算法:
由于数据的不断增加,单台redis内存已经不能满足实际需求,我们需要将数据分区。
+ 简单的hash取模分区:
通过散列函数将key变成固定长度数字,对其取模,取模结果对应第几台redis实例,但是这种方式不利于redis的扩展及删除,有局限性。
+ 一致性hash算法:
对于实时性不是特别高的场合,比如说T+1的报表,可以采用定时任务查询数据库数据,同步到redis中。
在一般情况下,我们推荐直接将删除redis的方案。
1.先更新数据库,再删除redis缓存
异常情况:更新数据库成功,删除redis失败,造成数据不一致!
解决方案:
1.1提供重试机制:
删除redis失败,捕获异常,将其发送到消息队列中,创建消费者再次尝试删除key,但是这样会对业务代码进行入侵
1.2异步更新缓存
更新数据时,会往binlog中写日志,可以通过一个服务监听binlog的变化,然后在redis中实现删除key操作,或者说直接发送到MQ中进行消费删除key操作。
总之,对于后删除失败的情况,我们的做法就是不断删除,知道成功!!
2.先删除缓存,在更新数据库
表面上没有问题,实际还可能出现问题:高并发情况下:线程A先删除redis,这时线程B过来发现redis没有,去数据库中查询旧值并更新到redis中,线程A再去更新数据库,这样还是会发生数据不一致情况!!
解决方案:
2.1延迟双删:
1>删除redis缓存
2>更新数据库
3>休眠500ms(根据实际情况而定)
4>再次删除缓存
在一般情况下,我们推荐直接将删除redis的方案。
1.先更新数据库,再删除redis缓存
异常情况:更新数据库成功,删除redis失败,造成数据不一致!
解决方案:
1.1提供重试机制:
删除redis失败,捕获异常,将其发送到消息队列中,创建消费者再次尝试删除key,但是这样会对业务代码进行入侵
1.2异步更新缓存
更新数据时,会往binlog中写日志,可以通过一个服务监听binlog的变化,然后在redis中实现删除key操作,或者说直接发送到MQ中进行消费删除key操作。
总之,对于后删除失败的情况,我们的做法就是不断删除,知道成功!!
2.先删除缓存,在更新数据库
表面上没有问题,实际还可能出现问题:高并发情况下:线程A先删除redis,这时线程B过来发现redis没有,去数据库中查询旧值并更新到redis中,线程A再去更新数据库,这样还是会发生数据不一致情况!!
解决方案:
2.1延迟双删:
1>删除redis缓存
2>更新数据库
3>休眠500ms(根据实际情况而定)
4>再次删除缓存
缓存刚开始为空,大量请求涌入数据库
缓存预热,提前将可能会访问的数据加载到缓存中
缓存降级,保留核心业务的功能
缓存和数据库双写出现一致性问题解决方法
预留缓存,写操作时会先删除缓存中的数据,再写入数据库,缓存再从数据库中拿数据
读写串行化
并发读写
单库
一致性问题:A删除了缓存还没写入数据库,B读缓存没有该数据,就从数据库中读取了,
之后A才完成写入数据库,B读到的就是脏数据
之后A才完成写入数据库,B读到的就是脏数据
根本原因:逻辑处理消耗1s
主从
一致性问题:A写入主库,从库还没来得及更新,B从从库中取出了脏数据之后从库才更新新的数据
根本原因:主从同步延时1s
无底洞优化
优化方法
1、命令本身优化
慢查询
配置
slowlog-max-len
先进先出队列、固定长度、内存
默认10ms, 建议1ms
slowlog-log-slower-than
建议1000
命令
slowlog get [n]
slowlog len
slowlog reset
生命周期
1、客户端发送命令到服务端
2、排灯等待执行
3、执行命令
属于慢查询
客户端超时不一定是慢查询,慢查询一定是客户端超时的一个原因
3、返回结果到客户端
两个配置
slowlog-max-len
1、慢查询是一个先进先出队列
2、是固定长度的
3、保存在内存中的
动态配置
config set slowlog-max-len 1000
slowlog-log-slower-than
1、慢查询阙值(微秒)
2、等于0,记录所有命令
3、<0 不记录所有命令
动态配置
config set slowlog-log-slower-than 10000
三个命令
1、slowlog get[n].
获取慢查询队列
2、slowlog len
获取慢查询队列长度
3、slowlog reset
清空慢查询队列
运维经验
1、slowlog-max-len不要设置过大,默认10ms,通常设置1ms
2、slowlog-log-slower-than不要设置过小,通常设置为1000
3、理解命令的生命周期
4、定期持久化慢查询
2、减少网络通信次数
3、降低接入成本
长链接
连接池
NIO(非阻塞IO技术)
缓存使用过程
前台请求,先从缓存中查询数据,缓存中没有则从数据库查询,查询数据后更新缓存,然后返回,下次查询直接从缓存中取值;
如果缓存和数据库中都没有直接返回空值。
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
热点key
缓存时间不失效
多级缓存
布隆过滤器
读写分离
二级缓存
备份热key 走不同的机器
大Key
bigkey命令 找到干掉
Redis 4.0引入了memory usage命令和lazyfree机制
并发竞争
分布式锁
扩展
跳跃表
令牌
漏桶
scan
其他
保护
spiped: SSL代理
设置密码
server: requirepass / masterauth
client: auth命令 、 -a参数
rename-command flushall ""
不支持config set动态配置
bind 内网IP
管道
RTT:往返延时(Round-Trip Time)
+ 管道的作用就是减少通信的次数,将很多指令一次发送过去执行,不必每次一条指令建立一次通信。
+ 返回:管道会回复一个队列答复,将所有的执行的结果以一个队列返回
+ 管道的作用就是减少通信的次数,将很多指令一次发送过去执行,不必每次一条指令建立一次通信。
+ 返回:管道会回复一个队列答复,将所有的执行的结果以一个队列返回
开发规范
kv设计
key设计
可读性、可管理型
简洁性
string长度会影响encoding
embstr
int
raw
通过 `object encoding k` 验证
排除特殊字符
value设计
拒绝bigkey
最佳实践
string < 10K
hash,list,set元素不超过5000
bigkey的危害
网络阻塞
redis阻塞
集群节点数据不均衡
频繁序列化
bigkey的发现
应用异常
JedisConnectionException
read time out
could not get a resource from the pool
redis-cli --bigkeys
scan + debug object k
主动报警:网络流量监控,客户端监控
内核热点key问题优化
bigkey删除
阻塞(注意隐性删除,如过期、rename)
unlink命令 (lazy delete, 4.0之后)
big hash渐进删除:hscan + hdel
选择合适的数据结构
多个string vs. 一个hash
分段hash
节省内存、但编程复杂
计算网站独立用户数
set
bitmap
hyperLogLog
过期设计
object idle time: 查找垃圾kv
过期时间不宜集中,避免缓存穿透和雪崩
命令使用技巧
O(N)命令要关注N数量
hgetall, lrange, smembers, zrange, sinter
更优:hscan, sscan, zscan
禁用危险命令
keys, flushall, flushdb
手段:rename机制
不推荐select多数据库
客户端支持差
实际还是单线程
不推荐事务功能
一次事务key必须在同一个slot
不支持回滚
monitor命令不要长时间使用
连接池
连接数
maxTotal
如何预估
业务希望的 redis 并发量
客户端执行命令时间
redis 资源:应用个数 * maxTotal < redis最大连接数
maxIdle
minIdle
等待
blockWhenExhausted
maxWaitMillis
有效性检测
testOnBorrow
testOnReturn
监控
jmxEnabled
空闲资源监测
testWhileIdle
timeBetweenEvictionRunsMillis
numTestsPerEvictionRun
备用方案
主从+哨兵+cluster
ecache+Hystrix+降级+熔断+隔离
持久化
Guava
Java项目广泛依赖,例如以下内容等
集合[Collections
缓存[caching]
原生类型支持[primitives support]
并发库[concurrency lib]
通用注解[common annotations]
字符串处理[String processing]
IO
Guava是对JDK集合的扩展
不可变集合:用不变的集合进行防御性编程和性能提升
新集合类型: multisets multimaps tables 等
强大的集合工具类:提供java.util.Collections中没有的工具类
扩展工具类:让视线和扩展集合类变得更容易,比如创建Collection的装饰器,或视线迭代器
Guava对集合的支持
只读设置
ImmutableList
函数式编程:过滤器
Collections2
函数式编程: 转换
Collections2.transform
组合式函数编程
加入约束: 非空、长度验证
集合操作: 交集、差集、并集
Multiset: 无序可重复
Multimap key : key 可以重复
BiMap: 双向Map 健值不能重复
双健的Map -->Table-->rowKey+columnKey + value
Guava Cache介绍
本质
封装了一个过期时间
过期策略
定时删除
定期删除
惰性删除
guava本地缓存就是采用惰性删除的方式
教程和源码
关键源码图
http://ifeve.com/google-guava
git@github.com:wstever/springboot-action.git
Caffeine
spring5已经放弃guava,拥抱caffeine。
guava和caffeine性能测试
spring5已经放弃guava,拥抱caffeine。
spring5已经放弃guava,拥抱caffeine。
那么为什么这么好的东西需要被淘汰呢,如果对于本地Cache有过深入研究的人应该知道LRU算法基本可以满足大部分的场景,但是很多人为了精益求精,基于LRU的算法,又在此基础上提出了一系列更好的,更有效果的淘汰策略。比如有ARC,LIRS和W-TinyLFU等都提供了接近最理想的命中率,他们这些算法进一步提高了本地缓存的效率。
Cache的目的就是缓存,如果在功能一样的情况下,最重要的突破就是性能了。从功能上来看GuavaCache已经比较完善了,基本满足了绝大部分本地缓存的需求。那么Spring5使用Caffeine来代替GuavaCache就是因为性能的问题了。首先我们来看看官方给出来的性能测试对比的报告。
Cache的目的就是缓存,如果在功能一样的情况下,最重要的突破就是性能了。从功能上来看GuavaCache已经比较完善了,基本满足了绝大部分本地缓存的需求。那么Spring5使用Caffeine来代替GuavaCache就是因为性能的问题了。首先我们来看看官方给出来的性能测试对比的报告。
三级缓存
背景
日常工作中,时常要面对抽奖活动,奖励发放,商品秒杀等大流量高并发的场景。
高并发场景面对的第一个问题是DB的IO瓶颈。 这时比较通用的方式是加缓存对DB进行加速与保护,用redis对内存的性能来解决IO的瓶颈。
但是引入Redis就一劳永逸了嘛?不是的,相对应的高并发场景又会引发Redis热KEY和大KEY的问题,造成带宽拉胯。
高并发场景面对的第一个问题是DB的IO瓶颈。 这时比较通用的方式是加缓存对DB进行加速与保护,用redis对内存的性能来解决IO的瓶颈。
但是引入Redis就一劳永逸了嘛?不是的,相对应的高并发场景又会引发Redis热KEY和大KEY的问题,造成带宽拉胯。
这时需要再做一层本地缓存。那么问题又来了,本地缓存没有过期时间等限制,现在可以用GUAVA CACHE来解决
缓存工具类
https://blog.csdn.net/qq_29817481/article/details/94021421
SpringBoot为我们提供了自动配置多个CacheManager的实现
SpringBoot为我们提供了自动配置多个CacheManager的实现
为什么能做缓存
读写速度快,减轻数据库压力,提高性能
怎么做缓存
放在请求访问和数据库之间,先操作缓存中的数据,
若缓存中不存在,则从数据库读取返回并更新至缓存中
若缓存中不存在,则从数据库读取返回并更新至缓存中
缓存使用过程
前台请求,先从缓存中查询数据,缓存中没有则从数据库查询,
查询数据后更新缓存,然后返回,下次查询直接从缓存中取值;
如果缓存和数据库中都没有直接返回空值。
缓存的收益和成本
收益
1、加速读写
通过缓存加速读写速度
CPU的L1、L2、L3CaChe
CaChe加速硬盘读写
EhCaChe缓存数据库结果
浏览器缓存
使用场景
利用redis优化IO响应时间
大量写入合并为批量写
计数器先累计,然后批量写入DB
2、降低后端负载均衡
后端服务器通过前端缓存降低负载:业务端使用redis降低后端mysql负载
使用场景
对高消耗的SQL:join结果集/分组统计结果
成本
1、数据不一致
缓存层和数据层有时间窗口不一致和更新策略有关
2、代码维护成本
多了一层缓存逻辑
3、运维成本
比如集群等
缓存的更新策略
1、LRU/LFU/FIFO算法剔除
maxmomory-policy
2、超时提出
expire
3、主动更新
开发控制生命周期
三种策略的比较
1、LRU/LFU/FIFO算法剔除一致性最差,同时维护成本也是最低的
2、超时剔除一致性稍微比第一种策略好一点,维护成本也低
3、主动更新一致性高,维护成本也高
建议
1、低一致性
最大内存和淘汰策略
2、高一致性
超时剔除和自动更新结合,最大内存和淘汰策略兜底
缓存的粒度控制
场景
1、从MySQl获取用户信息
select* from user where id=id
2、设置用户信息缓存
set user:{id} ‘select* from user where id=id’
3、缓存粒度
缓存全部
缓存部分
三个角度
1、通用性
全部属性最好
2、占用空间
部分属性最好
3、代码维护
全部属性最好
公共组件
http://10.219.14.24:6789/api-docs/redis/#/
Redis面试常见问题
Redis是什么?Redis的基本了解?
Redis有哪些数据类型
Hash
String
List
Set
ZSet
Redis的应用场景有哪些?
Redis的数据类型的各自应用场景?
分布式寻址都有哪些算法?
哈希槽 Redis集群时,怎么样set一个key
Redis Cluster集群模式,redis的key是如何寻址的?
了解一致性hash算法么?
Redis的缓存雪崩?
缓存如何是实现高并发的?
在中午高峰期,有100万用户同时访问系统A,每秒4000个请求去查询数据库
如果数据库每秒4000个请求,可能会宕机
如果数据库每秒4000个请求,可能会宕机
每秒4000个,3000个走缓存,1000个请求走数据库。
数据库支撑不了高并发,为什么缓存可以支撑高并发呢?
缓存是走内存的,内存天然就可以支撑别说是4000/s,4万/s也没问题
但是数据库一般建议不要超过2000/s
但是数据库一般建议不要超过2000/s
缓存是如何实现高性能的?
如何保证缓存与数据库双写一致性问题
只要用缓存,就会涉及缓存与数据库双写,只要是双写,就一定会有数据一致性的问题。
cache aside pattern 最经典的缓存+数据库读写模式
读的时候,先读缓存,缓存没有的话,按噩梦就读数据库,然后取出数据放入到缓存
更新的时候,先删除缓存,然后再更新数据库。
为什么是删除缓存,而不是更新缓存?
比如商品详情页的统计,可能只是修改了某个表的字段,但是统计可能还需要其他表传一些数据,然后再进行复杂的运算,导致更新缓存的代价很高。
或者一个表被频繁的修改,但是读数据没那么频繁。1分钟内修改了100次,那么缓存就要更新100次;但是缓存在一分钟内只被读取了一次,就有了大量的冷数据。
其实就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让他需要被使用的时候再重新计算。
或者一个表被频繁的修改,但是读数据没那么频繁。1分钟内修改了100次,那么缓存就要更新100次;但是缓存在一分钟内只被读取了一次,就有了大量的冷数据。
其实就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让他需要被使用的时候再重新计算。
最初级缓存不一致情况:
情况:先去数据库更新数据,然后删除缓存,但是删除缓存报错了没删掉,导致请求直接去缓存里拿到旧的数据,与新数据不匹配
解决:先删除缓存,再写入数据库,这样就算数据库更新失败报错,没有缓存也是去数据库拿旧数据,不会出现数据不一致情况
比较复杂的数据不一致情况
情况:
1.在更新一个库存的时候,同时在读取这个库存的缓存,并发的发生了。
2.开始执行操作,先删除缓存中的数据,但是还没来的及将库存在数据库中修改。
3.读请求去缓存里读,读不到,然后去数据库里读到旧数据,然后写到redis里。
4.这个时候才完成更新数据库系统的操作,导致数据库里的数据是新的,缓存里的数据是旧的。
--只有在高并发场景下,才会出现这种问题。
1.在更新一个库存的时候,同时在读取这个库存的缓存,并发的发生了。
2.开始执行操作,先删除缓存中的数据,但是还没来的及将库存在数据库中修改。
3.读请求去缓存里读,读不到,然后去数据库里读到旧数据,然后写到redis里。
4.这个时候才完成更新数据库系统的操作,导致数据库里的数据是新的,缓存里的数据是旧的。
--只有在高并发场景下,才会出现这种问题。
如图
解决
数据库与缓存的更新与读取进行异步串行化
1.先创建几个内存队列,根据商品的id,进行hash取值,然后根据队列数量取模,放到相应的队列里
2.每个队列通过异步一个线程进行消费,顺序消费队列里的请求。
这样的话,就能保证先完成数据库和缓存的修改,才能够进行读数据
2.每个队列通过异步一个线程进行消费,顺序消费队列里的请求。
这样的话,就能保证先完成数据库和缓存的修改,才能够进行读数据
如图
优化点
如果同一个数据有很多读请求过来,全都进行数据库的更新是没有意义的,
可以做过滤,如果发现队列中已经有一个更新缓存的请求了,就不用再放个更新请求操作进去了,直接等待前面的更细 操作请求完成即可
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
高并发场景下注意一些问题
(1)读请求长时阻塞
如果数据频繁更新,导致大量更新的操作在同一个队列里面,然后读请求等待很多的写请求完成后再执行,会发生大量超时,最后导致大量的请求直接走数据库。
一定要根据实际业务情况,去进行一些压力测试,和模拟线上环境,去看看这种情况下读多久能响应
如果不够的话就加机器
(2)读请求并发量高
还是要做好压力测试,一个大的风险就是突然间大量的读请求会在几十毫秒的延迟hang在服务上,看服务器能不能扛得住。需要多少机器才能扛住极限情况的峰值
(3)多台机器,要确保对同一个商品的读写请求,全部路由到同一台机器上,否则不在同一台机器上队列也就没用了
通过某个请求的参数hash进行路由到同一个机器上
(4)热点商品的路由问题,导致请求的倾斜
万一某个商品的请求读写特别高,全部打到相同的机器的相同队列里去了,可能造成某台机器压力过大
如果没有要求最好不要做这个串行化,因为一旦进行串行化,会导致系统吞吐量大幅度降低
可能就得用比正常情况下多几倍的机器去支撑
可能就得用比正常情况下多几倍的机器去支撑
用了缓存有啥后果?
缓存与数据库双写不一致
缓存并发竞争
0 条评论
下一页