redis作为目前比较主流的缓存数据库,一直是中高级后端工程师面试的宠儿。缓存可以提高用户查询的效率,可以应付数据库熔断的事件,为数据库层面减轻了压力。
目前,主流的数据请求应用架构基本都是 请求——>缓存层——>持久层。
用户发送请求先到缓存层查询数据,如果查询的数据没有的话,才会到数据库层面去查询数据。
穿透:缓存层没有对应的数据,向存储层发起请求。
熔断:当数据库突然发生不可预料的错误或者崩溃,此时,为了能继续向客户提供服务,会将请求打在缓存层,即使缓存层没有,也会向请求端返回响应,在一定程度上做到了即时止损。
mencache代码层次类似Hash,仅支持简单类型,不支持主从,不支持分片,不支持持久化。
redis数据类型丰富,支持主从分离,支持持久化,支持分片。
俗话说的好,没有最好的技术,只有更合适的场景,memcache适合在仅仅需要缓存而不需要持久化的条件下。redis适合于复杂条件下需要持久化以及主从分离的场景。
官方提供的Redis的查询效率是10W+QPS(每秒查询次数)。
原因是因为
redis有五种基本的数据类型
String、List、Hash、Set、Sorted Set
set key value//设置某个属性
get key//取出某个属性
set key value1 //可以通过该方式来更改key对应的value
del key //删除某个key
lpush list value //向key为list的列表中头部插入一个数据
lpop list value//向key为list的列表中头部拿出一个数据
rpush key value
rpop key value
hmset hash name "张三" age 18 sex "男"
hget hash age //取出hash中key为age的属性
sadd set1 "aaa"
sadd set1 "bbb" //向set中添加了aaa 和 bbb
smembers key //查看当前key的所有元素
zadd set1 2 "haha"
zadd set1 3 "heihei"//添加数据
zrengescore set1 0 10 //输出分数从0到1的set中的值
从海量数据中取出相同前缀的数据,需要先搞清楚数据量的大小,如果数据量比较小,则可以使用keys [pattern]
来直接获取匹配pattern的key,但是,这种方法会阻塞主进程,如果数据量比较大,则会出现服务器卡顿,如果这时服务器正在向其他用户提供服务的话,则会出现恶劣的影响。
数据量比较大时,我们可以使用SCAN cursor [match pattern] [count count]
迭代器来查找,其中cursor
是游标,每次迭代时都要输入上次的游标,第一次查询时要输入0,每次查询后,服务器会返回一定数量的key以及迭代到到什么位置了,这个数量可能小于等于count
,也可能不返回,每次返回的游标位置也可能比查询时的小,这就要解决重复key的问题了。直到迭代返回的游标为0的时候,才算结束了迭代。
使用游标返回key时,服务器不会有卡顿现象产生,但是使用游标迭代时,所花费时间要大于直接使用keys pattern
的查询方式。
实现分布式锁主要要解决以下问题
初始解决方法
使用setnx key value
可以设置当前key长期有效,且不能被更改(创建成功后返回1,此后修改会返回0表示失败),但是出现了一个问题,怎样去解决该key长期有效的问题,此时,我们可以使用expire key time
为当前的key设置过期时间,这样两个操作结合,就可以实现分布式锁了
问题:我们在Java中写一段程序
String status = getKey(key,value);//该操作获取锁
if (status == 1) {
setTime(key,time);//为其设置过期时间
//进行操作
}
乍一看这段伪代码没什么问题,但是仔细看就会发现,如果程序在获取锁的时候,还没执行到if那块的时候挂了,此时,获得了锁,但是该程序却永远也没办法解除锁了(关于为什么其他人不能去解锁,因为要保持安全性,上面有解释),这就形成了死锁。形成该情况的原因是虽然两个操作都是原子性的,但是合并在一起就不是原子性了。
解决方法:使用set key value [EX seconds] [PX milliseconds] [NX|XX]
其中EX和PX只需要设置一个,分别为秒和毫秒,后边的NX意为只在不存在的时候创建,XX与之相反只在存在的时候改变。
Redis有两种持久化方式,分别为RDB持久化以及AOF持久化
自动化保存:自动化保存会在四个条件下触发。
一、通过save n m时间策略进行保存(save 900 1 900秒内有一次修改操作,save 300 10 300秒内有10次改动 60 10000 1min内有10000次修改)。
二、主从复制时,redis会自动触发RDB持久化,为了将该文件拷贝到从属数据库上。
三、执行debug reload
四、执行shutdown,并且没有开启AOF持久化时
手动保存
一、通过 save
指令,此时数据库会在主进程内执行该方法,用来持久化数据,此方法会严重阻塞主进程,不建议使用。
二、通过bgsave
指令,此方法执行之后,Redis会(fork)生成一个子线程,该子线程会在后台进行持久化,并不会干扰到主进程的运行。此时,父子进程会执行一个copt-on-write策略(写时复制),redis子进程在进行复制时,父进程不会直接复制一份资源给子进程,而是与子进程同时使用相同的资源,这样可以让子进程选择要备份的数据,不会造成资源的浪费,当父进程要对资源进行修改的时候,系统会复制一份资源给父进程,供其操作。
三、bgsave
实现原理
父进程在生成子进程时会查看是否此时有AOF/RDB子进程是否正在进行,如果有则返回错误,这样是为了避免进行资源竞争。
缺点:内存数据的全量同步,如果数据量大的话会由于I/O频繁而严重影响服务器性能。如果redis挂掉了,会丢失最近一次快照到此时间节点的全部数据。
AOF会记录处理查询之外所有对数据进行变更的指令,并且保存在.aof文件中。指令会以追加的方式来写入到AOF文件(增量)。AOF默认是关闭的,需要通过redis.conf文件打开。
AOF配置策略(三选一)
appendfsync : always //如果有改变,总是将其写入
appendfsync : everysec//每秒写入一次
appendfsync : no //交给OS来管理,OS一般会等到缓存满了之后才进行写入
解决AOF日志大小不断增加的问题
原因:每当我们有一个改动操作时就会写入一个,但是,有时候我们并不需要那么多的指令,比如我们在使用 incr key
计数器时,我们只需要最后的那条指令,之前的指令载入只会浪费空间。
解决方法:使用AOF重写bgwriteaof
1、父进程调用fork方法生成一个子进程,子进程将当前全部数据生成AOF指令,并且写入新的AOF文件中。
2、父进程在子进程生成文件时,将新的AOF指令不断的写入缓存以及旧的AOF文件中(为了防止子进程失败)。
3、子进程写完之后通知父进程,父进程将缓存中的AOF再次发送新的增量变动。
4、子进程接收到新的增量之后再次写入新的AOF文件中,写完之后将该文件替换旧的AOF文件。
数据恢复非常的简单,只需要重启Redis-Server,重启之后,Redis会子自动加载AOF/RDB文件进行数据恢复。
RDB
优点:全量数据快照,文件小(二进制)恢复快
缺点:无法保存最近一次快照之后的数据
AOF
优点:可读性高,适合保存增量数据,数据不易丢失
缺点:文件体积大,恢复时间长
既然两者都有优缺点,我们是否可以取之优点来互补呢?
当然可以,redis4.0之后实现了这个方法,在进行持久化的时候,父进程fork一个子进程,之后子进程将当前全量数据保存下来并且写入一个新的AOF文件中(以rdb格式),同时父进程将增量数据保存在缓存中,当子进程告知父进程写完了,父进程会将增量数据发送给子进程,子进程再将增量写到新的AOF文件中。
此时的AOF文件,前半段是rdb格式的全量快照,后半段是增量数据的aof。
这个流程同AOF进行重写的时候非常相似。
我们在与redis服务器进行交互是发送一个请求,返回一个响应,如果我们要向服务器发送大批量的命令(请求),就会非常慢,这时,我们可以使用pipeline
批量上传指令,上传的指令之间不可以有依赖关系。如果有依赖关系则使用分批上传。
redis的主从同步是master与salve之间进行同步,其中又分为全同步和增量同步。
哨兵的存在是为了解决主从同步的时候,主master宕机之后的主从切换问题,哨兵主要进行三方面的作用。
Gossip算法
(流言协议)来进行主从切换,如果这时有其他请求发送到了瘫痪的master,哨兵进程会将新的master服务器地址返回给前台。Gossip算法(流言协议)
实现异步队列可以使用redis中的List,使用其RPUSH
和LPOP
来进行队列的实现。
rpush
产生消息,lpop
消费消息。
缺点:lpol没有实现等待消息队列中有值就直接消费(一般会设置一个轮询来不断的pop,但是如果队列中没有值的话,pop会报错)
解决方法:
1、使用sleep方法,过一段时间再去访问消息队列。
2、使用blpop key time
来实现定时pop,(如果在时间内队列中有值的话就接收,如果没有值的话就超时)一般还是设置一个循环来轮询
缺点:以上的队列虽然实现了等待消息队列中有值才消费,但是只能作用于一个消费者。
解决方法:使用pub/sub
订阅者模式实现多个消费者
subscroibe key //订阅
pulish key "信息" //向订阅中发送一个信息
俗话说,没有完美的技术,虽然这个能实现多个消费者通信,但是其缺点是消息的发布是无状态的,无法保证可达,如果想使用,最好使用kafka等专业的消息队列。
如何从海量数据中快速找到所需要的数据呢?如果只单纯的靠一个数据库,在大数据量十分庞大的今天,似乎已经行不通了,于是,大家就产生了分片的想法,按照某种规则去划分数据,分散存储到多个节点中,降低当前节点压力,数据少了,查的也就快了。
再具体的一致性哈希算法可以看看这篇文章一致性哈希算法