redis是单线程
多路IO复用
Redis数据结构
Redis锁
悲观锁:
操作之前必然先上锁,这就叫悲观锁。传统关系型数据库中用到很多:行锁,表锁,读锁,写锁,等。
乐观锁:
谁想操作谁操作,但是操作前必须将拿到的版本号和数据的版本号对比,一致才能操作,不一致说明拿到的数据已经旧了,此次操作不能进行。(适用于多读的场景)抢票:只有一张票,很多人都能抢到,但是只有一个人能支付成功。
【1、拿版本号,2、check and set 一致?操作:不能操作】
redis是利用check-and-set机制(乐观锁)实现事务:watch, multi, exec。
redis不能直接使用悲观锁。
Redis事务三特性
超卖问题:用连接池解决。
连接超时问题:2000个请求,redis不能短时间内处理过来,有部分请求被暂时搁置,然后他们就连接超时了。用redis事务(锁)解决(watch, multi, exec)(乐观锁)。
库存遗留问题:500件商品,2000个线程同时请求,只有一个线程拿到并先执行了,秒杀成功,其他线程进行秒杀操作的时候,对比版本号发现已经不对了,全部秒杀失败;所以最后2000个人同时秒杀,只有一个人秒杀成功,其他人全部秒杀失败,但是库存仍然有499个。LUA脚本解决(不再使用redis事务了)。(实际上是redis 利用其(redis)单线程的特性,用任务队列的方式解决多任务并发问题。执行脚本的时候一定会执行完脚本中的全部命令,这样其他线程就只能在java代码里走到redis执行lua脚本那一步然后等着,等着前一个线程执行完redis执行Lua脚本这一步,然后redis才能执行后面的。相当于悲观锁。)
lua脚本:
执行代码:
持久化
RDB
默认开启
RDB配置:
格式:save 秒数 写操作次数
如何禁用:1、不设置save规则,2、save “”
save 30 10 在30秒内,有10个键发生改变,就启动持久化
第11、12个发生改变的键不会被写入rdb文件,而是重新开始计算时间:30秒内有没有10个键发生改变。
若没有呢?
只有某个30秒内有10个键发生了改变,就开始将内存中的数据写入磁盘。
save:手动持久化:redis只管保存,其他不管,客户端请求会被阻塞。不建议。
bgsave:自动持久化:redis在后台异步进行快照操作,同时还可以相应客户端请求。
执行flushal命令,会产生空的dump.rdb文件,无意义。
stop-writes-on-bgsave-error yes :redis持久化,要把内存中的数据写进磁盘中,假如现在磁盘满了,那就关闭redis写操作(客户端的写命令就不能被正常响应了)
rdbchecksum yes :redis使用CRC64算法进行数据校验,校验数据完整性,如果数据已经损坏,那持久化也没有意义,恢复不过来。会增加约10%的性能消耗,但是能提升数据有效性。
RDB持久化过程:
1、备份:写时复制技术
为什么要往临时文件里写,而不直接向dump.rdb文件中写呢?
为了保护数据的安全性。防止写的时候宕机,从而有数据丢失的风险。这样即使redis宕机,也仍有之前的dump.rdb文件完好无损,不至于整个文件都损坏。
创建一个跟之前一样的空间,先复制之前的内容,写完后再替换之前的内容。这个就叫写时复制技术。
RDB缺点:最后一次持久化时,会造成数据丢失。(在制定时间间隔内,发生持久化,如果这时redis宕机,那么当前部分的数据就会丢失。(宕机后redis内存中没有,也没有写入到rdb文件中))
2、恢复
只要dump.rdb在启动redis的目录下(或者不在启动目录,只要在你redis.conf配置的目录下就可),将redis一启动,就会自动恢复数据,将备份文件直接写入内存中。
优势:
- 恢复速度快(一启动就恢复)
缺点:
- 数据丢失风险:redis意外宕机时,会导致最后一次持久化时的数据丢失
- 空间浪费:fork时,写时复制技术会导致2倍的膨胀空间(一份临时文件,一份原有的dump.rdb文件)
适用于:1、大规模数据恢复,2、对数据完整性一致性要求不高
总结:
1、RDB是一个非常紧凑的文件
2、RDB持久化方式在进行持久化时,redis唯一需要做的事就是fork一份子进程,剩余全部由子进程做。父进程无需其他IO操作,所以RDB持久化方式能最大化redis性能。在数据庞大时较消耗性能,但比AOF快一点(它里面直接是紧凑的数据,而AOF是一条条命令,命令有很多不是数据的最终态,要一条条执行)。
AOF
默认不开启
append only file
只在文件中进行追加
redis重启后就根据日志文件内容将命令从前往后执行一次以完成数据恢复工作。
AOF配置:
appendonly on
appendfilename “appendonly.aof”
RDB和AOF的文件位置是同一个配置
aof同步频率
aof从appendonly配置打开后,重启redis后生效。也就是说如果之前没有开启aof(不管有没有开启rdb),那么开启aof之后,数据是从重启之后才开始进行aof持久化,之前的数据不会进入aof文件中。
1、备份
客户端的每次请求写命令都会被追加到aof缓冲区内;然后根据aof持久化策略(always,everysec,no)将命令同步到aof磁盘文件中(追加);当aof文件超过重写策略或手动重写时,会对aof文件进行重写(fork写时复制。),压缩aof文件容量。
2、恢复
redis重启时自动加载配置的aof文件,读取其中内容,自动执行一遍以恢复数据。
重写策略:
auto-aof-rewrite-percentage 100 :设置重写的触发百分比,100%
auto-aof-rewrite-min-size 64mb :设置重写的基准值,64mb。这里把他叫做baseSize
这两条的意思是,文件达到64mb开始重写,重写后文件减小。那下次从什么时候开始重写呢?baseSize + baseSize * 100%,也就是下次达到128mb开始重写。那下下次呢?达到256mb还是192mb?
重写过程:
…
fork出子进程,
aof文件异常修复:
如果aof文件发生损坏,可以执行redis-check-aof命令进行修复:
redis-check-aof --fix appendonly.aof
修复完成后,启动redis即可自动重新加载aof文件。
redis能对aof文件进行后台重写,使得aof文件体积不会过大。
主从复制,读写分离
详情查看视频:https://www.bilibili.com/video/BV1Rv41177Af?p=32
一主三从,主从复制也是跟MySQL差不多一样,用的是redis本身的,我们只需要配置即可。他这个需要三份配置文件就好了,不用像MySQL那样把文件夹全都复制一份。三份配置文件,然后用redis-server 分别制定三份配置文件启动即可。启动成功后,三个都是主服务器(默认都是Master),只需在想要设置为从服务器的机器上执行命令:slaveof 主服务器的ip 主服务器的port,例如:slaveof 127.0.0.1 6379。
这样配置完成后,就好了。只有主服务器可以进行写操作,从服务器只能进行读操作(自动复制)(写操作会报错)。
一主二仆:当有一台从服务器挂到了,再手动启动起来后,它并不会自动变成从服务器,而是一个主服务器,需要手动再进行 slaveof ip port 命令才会变成从服务器。并且变成从服务器之后,会从头开始复制所有主服务器有的数据。
当主服务器挂掉后,从服务器还是从服务器,它知道自己的主服务器已经挂掉了,但是不做任何改变。当主服务器重新启动后,它还是主服务器,还是拥有原先的那两个从服务器。
薪火相传:主只能有一个,从却可以分等级。就像企业管理那样。一个老板发布命令,但是老板只对它手下的两个从发布命令,由这两个从再向他们手下的从发布。在最底层的从服务器C上使用命令slaveof ip port 制定它的老大B,指定后那个服务器B也还是从,但是最底层的服务器C已经不是A的从了,此时A服务器只有B一个从,B服务器有C一个从,但是B还是从,C也还是从。
反客为主:当一台主服务器挂掉后,在某个从上面执行slaveof no one,即可将他变为主服务器,其他从服务器不会有任何改变(是它的从的就是他,不是它的就不是它)。(哨兵模式是这个的自动版)
主从复制只能是一主一从或者一主多从;因为一旦多主,从服务器就不知道到底以那个主服务器数据为准了。
可以搞集群,集群中的每个节点都是一主多从,然后多个节点互相通信,当某个节点的主挂掉后,会继续从其他节点拿数据。
主从复制原理:
1、从发起,主服务器全量推送数据
2、主服务器增量同步数据
哨兵模式:单独启动一个redis哨兵应用,它还是用redis启动,但是已经不是redis应用了,提供的是redis哨兵服务。
1、新建sentinel.conf(名字不能错)
2、内容为:sentinel monitor mymaster 127.0.0.1 6379 1
其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
3、执行redis-sentinel /myredis/sentinel.conf命令
当主机挂掉,大概10秒,哨兵会同意服务迁移,然后从从服务器中挑选出新的主服务器。
新版本的配置不叫这个名字(slave-priority,叫replica-priority)
2、是指谁拥有的数据多,选择谁(主从复制有延迟,当主挂掉后,从服务器复制出来的数据不一定一样。)
3、启动时生成随机40位的runid
缓存更新策略
1、内存淘汰:Redis自己由内存淘汰策略,无人工维护成本
2、超时剔除:给缓存添加TTL过期时间,到期自动删除
3、主动更新:编写业务逻辑,在修改数据库的同时更新缓存。
对于数据高一致性需求:主动更新,并以超时剔除作为兜底方案。
主动更新策略:
1、Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存。
2、Read/Write Through Pattern:缓存和数据库整合为一个服务,由服务来维持一致性。调用者只需调用服务,无需关心缓存一致性。
3、Write Behind Caching Pattern:调用者只操作缓存,由其他线程异步的维持缓存与数据库数据的一致。
企业中用的较多的是第一种。
操作缓存和数据库时有三个问题需要考虑:
1、删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都要更新缓存,若更新了100次数据库,还没有一个查询请求,那么这些更新都是无效更新。
- 删除缓存:更新数据库时让删除缓存,查询时再更新缓存。相当于是懒加载的思想。
2、如何保证缓存和数据库操作的同时成功或失败?
- 单体系统:将缓存和数据库操作放在一个事务中。
- 分布式系统:利用TCC等分布式事务方案。
3、线程安全问题:先操作缓存还是先操作数据库。
- 先删除缓存,再更新数据库。
- 先更新数据库,再删除缓存。
都有可能发生线程安全问题,(一个查询请求,一个更新请求两个线程并发执行。)导致查出旧数据(缓存和数据库数据不一致)。但是总体来讲第二种可能性更小一些,所以更推荐。
缓存三大问题
缓存穿透
https://www.bilibili.com/video/BV1Rv41177Af?p=40
情况:redis缓存和mysql数据库中都不存在的key,穿透了缓存、数据库。高并发情况下,会压垮数据库,导致系统崩溃。
比如用一个不存在的用户id去查询用户信息。
解决方案:
- 1、对空值缓存,如果一个查询结果返回数据为空,那我们扔把这个空结果缓存,不过过期时间会很短,最长不超过5分钟。
- 2、布隆过滤器,快速过滤掉不存在的key,但是有一个问题就是布隆过滤器不能真正判断出key真的存在。
- 3、设置可访问的白名单
- 4、实时监控,当发现Redis命中率开始极速降低时,需要排查访问对象和访问的数据。必要情况下设置黑名单限制。
缓存击穿
情况:key对应数据存在,但redis中过期了,此时若有大量并发请求进来,这些请求是直接打到数据库的,可能会瞬时把数据库打垮。
解决方案:一般出现这种情况的数据都是“热点”数据,这些数据需要考虑缓存击穿问题。
- 1、预热热门数据:redis高峰访问前,把热门数据存入redis中,并加大过期时长。
- 2、互斥锁:分两步设置
- a、先setnx mutex value,就是仅当mutex没有设置时,设置一个mutex,value(mutex统一,value无所谓。比如setnx updateUsr1213 1)
- b、上一步操作成功时,再进行load db,并设置缓存,然后删除mutex,value;若上一步失败,说明已有线程在load db,则当前线程需要睡眠一段时间再整体重试get方法。
- 3、实时调整,监控热门数据,实时调整key的过期时长
缓存雪崩
情况:大量key集中过期,许多请求都打到数据库,大并发请求可能会瞬时把DB压垮。(击穿和雪崩的区别是前者是某一个热点key,后者是大量key集中过期)
这种情况相对来说是最容易发生最可怕的。
解决方案:
- 1、构建多级缓存(nginx缓存,redis缓存,其他缓存)
- 2、记录过期标志更新缓存:记录缓存是否过期,在快过期时触发另外的线程在后台更新实际的key的缓存值。
- 3、将缓存失效时间分散开:在失效时间基础上添加一个随机值(1~5分钟),每个缓存失效时间重复程度就会降低。避免集体失效事件。
最好的解决方案:
分布式锁
https://www.bilibili.com/video/BV1Rv41177Af?p=43
是什么?
在分布式环境中,在一台机器上上锁,其他机器不知道这个锁的存在,这个锁就对其他机器无效。分布式锁就是让一个锁使得其他所有机器都认识,也就是共享锁。
几种分布式锁的实现方案:
死锁问题
不加过期时间可能会导致死锁发生
上锁
setnx key value
expire key time
或者
set key value nx ex time(一条指令,具有原子性)
释放锁
del key
注意:使用setnx设置的key,如果使用set key的方式,还是能够设置进去;只有使用setnx才能有效。就是说setnx只针对setnx指令有效,对set无效。
锁误删的问题
一个客户端释放另一个客户端加的锁
解锁原子性问题
总结分布式锁:
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。(redis,中心服务器)
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。(设置过期时间)
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。(uuid)
- 加锁和解锁必须具有原子性。(加锁和解锁的过程必须原子)
总结:
1、锁必须设置过期时间,防止死锁,且必须在上锁时设置过期时间(原子操作set nx ex)。(一个客户端在持有锁期间崩溃没有主动释放锁)
2、锁的value必须唯一(可以使用uuid),避免误解锁。(一个客户端把另一个客户端的锁解了:a客户端加锁并设置过期时间为10秒,然后操作,过程中服务器卡住了,10秒之后过期时间到了自动释放了锁;此时b客户端就能加锁了,然后它正常操作。就在这时a服务器反应过来了,然后往下走,最终走到了解锁操作,a就会把b加的锁解开了。)
3、判断锁value和解锁这两个步骤必须保证原子性,使用Lua脚本。避免高并发情况下出现线程安全问题。(在判断完uuid后,准备删,还没有删的时候;锁过期,自动释放;其他机器拿到锁,进行操作;此时上一台机器自动释放。就呈现了解锁不原子性的问题:判断要不要删除操作和实际删除操作不是原子的。)
加锁:
set nx ex lockName uuid
解锁:KEYS[1]是lockName,ARGV[1]是uuid
if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end
分布式锁是在分布式情况下对统一资源的操作加锁,保证只有一个服务在操作公共资源。因为普通的锁(sync,lock)只是在同一个服务器层面的,分布式情况下不同服务都在不同机器上,就算同一个服务也有集群多台服务器,所以普通锁是没法锁住的。
redis中的过期数据是怎么没的?
首先,redis中设置了过期时间的key,value是一到过期时间马上被删除吗(set key value ex time)?
不是!
如果是这样的话,那redis岂不是要为每个设置了过期时间的key造一个定时器,然后定时结束立马去删除它?这样的成本太高了,CPU也忙不过来。
那是怎么删除的呢?两种方式:
1、定时删除:每个一段时间,随机抽查设有过期时间的Key,如果过期了,就删除它。
2、惰性删除:过期了不管他,下次get时,如果get的过期的Key,那么返回不存在,并删除该key,value。(就是懒更新、懒加载的策略)
以上两种方式都有弊端,第一种对CPU不友好;第二种对内存不友好(如果好多过期的key,永远都没有人去get他们,那他们岂不是永远留在了内存中?这对redis这样的内存数据库是致命的)
所以引来了第三种方式:
3、定期删除
选择一个时间周期,然后去通过内存淘汰策略筛查出那些需要淘汰的内存,然后执行删除。
redis的内存回收策略
1、maxmemory-policy noeviction(默认):内存空间不足会报错
2、allkeys-lru:最少使用的数据去淘汰
3、allkeys-random:随机淘汰一些key
4、volatile-random:在已经设置了过期的时间去随机淘汰
volatile-lru:在已经设置了过期的时间去淘汰最少使用的数据
volatile-ttl:在已经设置了过期的时间去淘汰即将过期的key