为什么要使用redis呢?
原因一、避免反复请求数据库造成效率低下
针对这种信息不经常变动并且数据量,为了提高访问效率,将其加入缓存中,避免频繁访问数据库
原因二、需要在 秒杀,热点数据和DB数据库之间加入缓存中间件
深层次原因同原因一
Redis 也就是NoSQL数据库
缓存中间件 Memcache和Redis的区别
Memche 代码层次类似Hash
缺点:
支持简单数据类型,只有String类型
每个key保存的数据量小只有1M
不支持数据持久化存储,把数据全部存在内存中,数据不能超过内存大小
不支持主从
不支持分片
优势:
Window和linux都可以使用,各种框架也支持
Session的信息可以非常方便的保存到改memcache中
Redis
数据类型丰富
支持数据磁盘持久化存储
支持主从
支持分片
Memche 和 Redis 都支持分布式
redis分布式:主从模式
memcache分布式:把key平均分配到各个服务器,addServer(主机名,端口);
Memcache介绍:/lxf2323881/article/details/79273465
为什么Redis能这么快
C语言编写,完全基于内存,绝大部分请求时纯粹的内存操作,执行效率高
数据结构简单,对数据操作也简单
采用单线程,单线程也能处理高并发请求,想多核也可启动多实例
使用多路I/O复用模型,非阻塞IO
要注意的是:Redis直接自己构建了VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
多路I/O复用模型
FD:File Descriptor 文件描述符
一个打开的文件通过唯一的描述符进行引用该描述符是打开文件的元数据到文件本身的映射
/zwjyyy1203/article/details/97105647
传统的阻塞I/O模型
多路I/O复用模型
Select系统调用
Redis采用的I/O多路复用函数
epoll/kqueue/evport/select ?
因地制宜,根据平台不同采用不同的策略
优先选择时间复杂度为O(1)的I/O多路复用函数作为底层实现
以时间复杂度为O(n)的select作为保底
基于react设计模式监听I/O事件
说说你用过的Redis的数据类型
String
Hash
List
Set
Sorted Set
用于记基数的HyperLog
用于支持存储地理位置信息的Geo
底层数据类型基础
简单动态字符串链表字典跳跃表整数集合压缩列表对象
从海量Key里查询出某一固定前缀的key
首先要摸清数据规模,问清楚边界
注意:使用keys对线上业务的影响
KEYS pattern: 查找所有符合给定模式pattern 的key
准备测试数据
keys k1* 查找k1开头的所有key
Keys的缺点一次性返回所有匹配的Key,键太多会导致服务卡顿
用SCAN 取代keys在生产环境中使用
SCAN cursor [MATCH pattern] [COUNT count]
基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
以0作为游标开始一次新的迭代,知道命令返回游标0完成一次遍历,以0开始到0结束 不保证每次执行都返回某个给定数量的元素
一次返回的数量不可控,只能是大概率符合count参数
例如:Scan 0 match k1count 10*
可能出现重复数据要去重
如何通过Redis实现分布式锁
分布式锁需要解决的问题
互斥性
安全性
死锁
容错
使用setnx实现分布式锁
SETNX key value: 如果key不存在,则创建并赋值
时间复杂度:O(1)
返回值:设置成功,返回1;设置失败,返回0
setnx locknx task
get locknx
后续线程如何获取到锁
如何解决SETNX长期有效的问题
Expire key seconds
设置key的生存时间,当key过期时(生存时间为0),会被自动删除
设置过期时间
expire locknx 2
在程序中如何实现
RedisService redisService = SpringUtils.getBean(RedisService.class);long status = redisService.setnx(key,”1”);If(status==1){redisService.expire(key,expire);//执行独占资源逻辑doOcuppiedWork();}
上面方法的缺点是原子性得不到满足*,一旦有一步挂了会导致后续问题*
上面的问题可以通过
SET key value [EX seconds] [PX milliseconds] [NX|XX]
来解决
EX second: 设置键的过期时间为second秒
PX millisecond: 设置键的过期时间为millisecond毫秒
NX:只要键不存在时,才对键进行设置操作
XX:只在键已经存在时,才对键进行设置操作
SET操作成功完成时,返回OK,否则返回nil
例子:set locktarget 12345 ex 10 nx
程序中可以通过以下代码实现:
RedisService redisService = SpringUtil.getBean(RedisService.class);String result = redisService.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);If(“OK”.euqals(result)){//执行独占资源逻辑doOcuppiedWork()}
大量的key同时过期的注意事项
集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象
解决方案:在设置key的过期时间的时候,给每个key加上随机值
如何使用Redis做异步队列
第一种方法:使用List作为队列,RPUSH生产消息,LPOP消费消息
缺点:没有等待队列里有值就直接消费
弥补:可以通过在应用层引入Sleep机制去调用LPOP重试
实例:
rpush textlist aaa
(integer) 1
rpush textlist bbb
(integer) 2
rpush textlist ccc
(integer) 3
lpop testlist
“aaa”
lpop testlist
“bbb”
lpop testlist
“ccc”
第二种方法:BLPOP key [key…] timeout: 阻塞直到队列有消息或者超时
实例:
blpop testlist 30
”testlist””aaa”
缺点:只能供一个消费者消费
第三种方法:pub/sub: 主题订阅者模式
发送者(pub)发送消息,订阅者(sub)接收消息
订阅者可以订阅任意数量的频道
实例:
订阅频道:subscribe myTopic
发布信息:publish myTopic(频道名) “Hello”(发布信息)
缺点是消息发布时无状态的,无法保证可达,要解决要使用消息队列
Redis如何做持久化
RDB(快照)持久化:保存某个时间点的全量数据快照
SAVE: 阻塞Redis的服务器进程,直到RDB文件被创建完毕
BGSAVE: Fork出一个子进程来创建RDB文件,不阻塞服务器进程
SAVE或BGSAVE命令两个命令的区别在于,前者是由主进程进行快照操作,会阻塞住其他请求,后者会通过fork子进程进行快照操作。
自动化触发RDB持久化的方式
根据redis.conf配置里的SAVE m n定时触发 (用的是BGSAVE)
主从复制时,主节点自动触发
执行Debug Reload
执行Shutdown且没有开启AOF持久化
BGSAVE原理
系统调用fork(),创建进程,实现了Copy-on-Write
RDB 保存过程
当条件满足,redis需要执行RDB的时候,服务器会执行以下操作:
redis调用系统函数fork() ,创建一个子进程进行持久化。子进程将数据集写入到一个临时 RDB 文件中(持久化,也就是将内存中的数据写入临时文件)。当子进程完成对临时RDB文件的写入时,redis 用新的临时RDB 文件替换原来的RDB 文件,并删除旧 RDB 文件。
注:fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是一个全新的进程,并作为原进程的子进程
在执行fork的时候操作系统(类Unix操作系统)会使用写时复制(copy-on-write)策略,即fork函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(如执行一个写命令 ),操作系统会将该片数据复制一份以保证子进程的数据不受影响,所以新的RDB文件存储的是执行fork那一刻的内存数据。
Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的。这使得我们可以通过定时备份RDB文件来实 现Redis数据库备份。RDB文件是经过压缩(可以配置rdbcompression参数以禁用压缩节省CPU占用)的二进制格式,所以占用的空间会小于内存中的数据大小,更加利于传输。
Copy-on-Write
如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者锁见到的最初的资源仍然保持不变
RDB持久化
缺点:
内存数据的全量同步,数据量大会由于I/O而严重影响性能
可能会因为Redis挂掉而丢失从当前至最近一次快照期间的数据
AOF(Appen-Only-File)持久化:保存写状态
记录下除了查询以外的所有变更数据库状态的指令
以append的形式追加保存到AOF文件中(增量)
AOF持久化
日志重写解决AOF文件大小不断增大的问题,原理如下:
调用fork(),创建一个子进程
子进程把新的AOF写到一个临时文件里,不依赖原来的AOF文件
主进程持续将新的变动同时写到内存和原来的AOF里
主进程获取子进程重写AOF的完成信号,往新AOF同步增量变动
使用新的AOF文件替换掉旧的AOF文件
AOF的设置方法:
为了打开 AOF 持久化的功能,我们只需要将 redis.conf 配置文件中的appendonly配置选项设置为yes即可。涉及 AOF 持久化的几个常用配置如下所示:
appendonly yes
appendfilename “appendonly.aof”
appendfsync everysec
appendonly:是否打开 AOF 持久化功能
appendfilename:AOF 文件名称
appendfsync:同步频率
同步频率
always每个 Redis 命令都要同步写入硬盘。这样会严重降低 Redis 的性能
everysec每秒执行一次同步,显式地将多个写命令同步到硬盘
no让操作系统来决定应该何时进行同步
RDB和AOF的优缺点
RDB优点:全量数据快照,文件小,恢复快
RDB缺点:无法保存最近一次快照之后的数据
AOF优点:可读性搞,适合保存增量数据,数据不易丢失
AOF缺点:文件体积大,恢复时间长
RDB-AOF混合持久化方式
BGSAVE做镜像全量持久化,AOF做增量持久化
使用Pipeline的好处
Pipeline和Linux的管道类似
Redis基于请求/响应模型,单个请求处理需要一一应答
Pipeline批量执行指令,节省多次IO往返的时间
有顺序依赖的指令建议分批发送
Redis的同步机制
全同步过程
Slave发送sync命令到Master
Master启动一个后台进程,将Redis中的数据快照保存到文件中
Master将保存数据快照期间接收到的写命令缓存起来
Master完成写文件操作后,将该文件发送给Slave
使用新的RDB文件替换旧的RDB文件
增量同步过程
Mater接收到用户的操作指令,判断是否需要传播到Slave
将操作记录追加到AOF文件
将操作传播到其他Slave: 1. 对齐主从库,2. 往响应缓存写入指令
将缓存中的数据发送给Slave
Redis Sentinel
解决主从同步Master宕机后的主从切换问题
监控:检查主从服务器是否运行正常
提醒:通过API向管理员或者其他应用程序发送故障通知
自动故障迁移:主从切换
(投票机制)
流言协议Gossip
在杂乱无章中寻求一致
每个节点都随机地与对方通信,最终所有节点的状态达成一致
种子节点定期随机向其他节点发送节点列表以及需要传播的信息
不保证信息一定会传递给所有节点,但是最终会趋于一致
Redis的集群原理
如何从海量数据里快速找到所需?
分片:按照某种规则去划分数据,分散存储在对个节点上
常规的按照哈希划分无法实现节点的动态增减,所以使用如下哈希算法:
一致性哈希算法:对2的32次方取模,将哈希值空间组织成虚拟的圆环:
将数据key使用相同的函数Hash计算出哈希值
请列举几个用得到Redis的常用使用场景
缓存:再提升服务器性能方面非常有效;排行榜:利用Redis的SortSet解决计算器/限速器:利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力好友关系:利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;简单消息队列:除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;Session共享:以PHP为例,默认Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。一些频繁被访问的数据:经常被访问的数据如果放在关系型数据库,每次查询的开销都会很大,而放在redis中,因为redis 是放在内存中的可以很高效的访问简述Redis的数据淘汰机制
volatile-lru从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru从所有数据集中挑选最近最少使用的数据淘汰
allkeys-random从所有数据集中任意选择数据进行淘汰
noeviction禁止驱逐数据
RDB的配置方式
找到Redis的配置文件:redis.conf
1) 设置触发条件:
2) 设置rdb文件路径
默认rdb文件存放路径是当前目录,文件名是:dump.rdb。可以在配置文件中修改路径和文件名,分别是dir和dbfilename
缓存更新方式
这是决定在使用缓存时就该考虑的问题。
缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。更新的方式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。
当数据源不是 DB 而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。
这种场景下,可以选择失效更新,key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。
但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用。改进的办法是异步更新,*就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。*这样就避免了失效瞬间的空窗期。另外还有一种纯异步更新方式,定时对数据进行分批更新。实际使用时可以根据业务场景选择更新方式
数据不一致
产生的原因
一般是主动更新失败,例如更新 DB 后,更新 Redis 因为网络原因请求超时;或者是异步更新失败导致。
解决的办法
如果服务对耗时不是特别敏感可以增加重试;
如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。
缓存穿透:redis和数据库中都没有数据
解决方法
1.接口层增加校验,对传参进行个校验,比如说我们的id是从1开始的,那么id<=0的直接拦截;
2.缓存中取不到的数据,在数据库中也没有取到,这时可以将key-value对写为key-null,这样可以防止攻击用户反复用同一个id暴力攻击,等再访问这个key的时候,就提示位置错误、稍后重试这样的值具体取啥问产品,或者将缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。
3.这样可以防止攻击用户反复用同一个id暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的,可以让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑。
4、还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return
缓存击穿:就是一个很热门的数据,突然失效,大量请求到服务器数据库中 单个key失效
最好的办法就是设置热点数据永不过期,拿到刚才的比方里,那就是你买腾讯一个永久会员
缓存雪崩:所有缓存同时失效
1.缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2.如果缓存数据库是分布式部署,将热点数据均匀分布在不同得缓存数据库中。
过期时间设置
EXPIRE将key的生存时间设置为ttl秒
PEXPIRE将key的生存时间设置为ttl毫秒
EXPIREAT将key的过期时间设置为timestamp所代表的秒数的时间戳
PEXPIREAT将key的过期时间设置为timestamp所代表的毫秒数的时间戳
其实以上几种处理方式都是根据PEXPIREAT来实现的,设置生存时间的时候是redis内部计算好时间之后在内存处理的,最终的处理都会转向PEXPIREAT。
1、2两种方式是设置一个过期的时间段,就是咱们处理验证码最常用的策略,设置三分钟或五分钟后失效,把分钟数转换成秒或毫秒存储到redis中。
3、4两种方式是指定一个过期的时间 ,比如优惠券的过期时间是某年某月某日,只是单位不一样
键过期时间查看
ttl:以秒为单位,返回键的剩余生存时间。
pttl:以毫秒为单位,返回键的剩余生存时间。
键过期判定
检查当前Unix(当前时间)时间戳是否大于键的过期时间,是则过期,否则不过期。