传统关系型数据类已经不能适应于所有场景了,比如秒杀扣减库存,App首页的访问流量高峰等等,都容易让数据库崩溃,所以需要引入缓存中间件。目前比较常用的缓存中间件由redis和memcached,结合其优缺点,最终选择redis。
redis自身是一个map类型的存储方式,其中所有的数据都是采用key:value的形式存储
我们讨论的数据类型只的是存储的数据类型,也就是value部分的类型,key部分永远都是字符串
字符串String,字典hash,列表List,集合set,有序集合SortedSet.
(3条消息) 【Redis】五种数据类型及其使用场景_愿万事胜意-CSDN博客_redis五种类型使用场景
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩。因此我们一般需要在过期时间上加一个随机值,使得过期时间分散一些。
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间,防止忘记释放锁。
若在setnx之后,expire之前,进程意外crash或者要重新维护了会怎样,怎么办?
锁没法释放了,不过可以同时把setnx和expire合成一条指令来使用,避免这个问题。
如果这个redis正在给线上的业务提供服务,那使用keys指令会造成什么影响?
redis是单线程的。Keys指令会导致线程阻塞一段时间,线上服务会停止,直到指令执行完毕,服务才会恢复。这个时候可以使用SCAN指令(增量式迭代命令—缺点:在对键进行增量式迭代过程中,键可能会被修改,所以增量式迭代命令只能对被返回的元素提供有限的保证),该指令可以无阻塞的提取出指定模式的Key列表,但是有一定的重复概率,不过没关系,我们可以去客户端做一次去重。不过整体花费的时间比keys指令长。
一般使用list结构作为队列,rpush生产消息,lpop消费消息,当lpop没有消息的时候,要适当sleep一会在重试。
若不使用sleep那?—使用list的bloop,在没有消息的时候,它会阻塞直到消息的到来。
如何生产一次消费多次----使用Pub/sub主题订阅者模式,可以实现1:N的消息队列
那Pub/sub主题订阅模式有什么缺点?—在消费者下线的时候,生产的消息会丢失,需要使用专业的消息队列如rocketMQ等。
使用sortedset,将时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore指令获取n秒之前的数据,轮询进行处理。
rdb做镜像全量持久化,aof做增量持久化。因为rdb会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof配合使用。在redis实例重启时,会使用rdb持久化文件重新构建内存,再使用aof重放最近的操作指令来实现完整恢复重启之前的状态。(理解:将rdb理解成一整个表全量的数据,AOF理解为每次操作的日志,服务器重启时先把表的数据全部导进去,但是可能不完整,因此还需要回放一下日志,是数据完整)redis本身机制:AOF持久化开启且存在AOF文件时,优先加载AOF文件,AOF关闭或者AOF文件不存在时,加载RDB文件,加载AOF/RDB文件后,redis启动成功,当AOF/RDB文件存在错误,redis启动失败并打印错误信息。
机器断电会怎样?—取决于AOF日志sync属性的配置,如果不要求性能,在写每条指令时都sync一下磁盘,就不会丢失数据,但是在高性能的要求下,不现实,一般使用定时sync,比如一秒一次,这时候最多丢失1s的数据。
RDB原理—fork and cow。fork是指redis通过创建子进程来进行RDB操作,cow时copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。(AOF/RDB原理和优缺点下来去掌握)
redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续的修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放,就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog.
redis sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
redis cluster着眼于扩展性,在单机redis内存不足时,使用cluster进行分布式存储。
目前电商首页以及热点数据都会去做缓存,一般缓存都是定时任务去刷新,或者是查不到之后去更新的,这里定时任务刷新就有一个问题。举个简单的例子。如果首页的key失效时间都是12小时,中午12点刷新的。现在0点有个秒杀活动大量用户涌入,假设当时每秒6000个请求,本来缓存存在可以扛住每秒5000个请求,但此时缓存中所有的key都失效了。此时1秒6000个请求全部落在了数据库,数据库会扛不住,重启,但重启的数据库立马又会被新的流量打死。这就是我理解的缓存雪崩。
如何解决?
①在批量往redis存数据的时候,把每个key的失效时间都加个随机值,这样就保证数据不会在同一时间大面积失效。
setRedis(key,value,time + Math.random()*10000);
②如果redis是集群部署,将热点数据均匀分布在不同的redis库中也能避免全部失效的问题(不过一般在项目中做集群的时候,单个服务都是对应单个redis分片,是为了方便数据管理,但也同样有了雪崩的弊端,失效时间随机是个很好的策略)
③设置热点数据永远不过期,当有更新时就更新缓存就好了。(电商首页数据就可以用这个操作)
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。比如我们数据库的id都是1开始自增上去的,如发起id值为-1的数据或者id特别大不存在的数据。造成数据库压力过大,严重会击垮数据库。
解决----在接口层增加校验,比如用户鉴权校验,参数校验,不合法的参数直接return,比如:id做基础校验,id<=0的直接拦截等。
从缓存中取不到的数据,在数据库中也取不到,这是可以将对应的Key的value写为null,位置错误,稍后重试这样的值。并设置一个缓存失效时间,缓存有效时间可以设置短点,这样就不会多次请求数据库了,第二次就会直接返回null.
采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤。
缓存击穿是指一个KEY非常热点,在不停的扛着大并发,大并发集中对这个KEY进行访问,当这个KEY在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。
解决:
①设置热点数据永远不过期
②在访问key之前,采用setnx来设置另一个短期key来锁住当前key的访问,访问结束后再删除该短期key.
一般避免上述4,5,6情况发生我们可以从三个时间段去分析
事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
事中:本地ehcache缓存 + Hystrix 限流+降级,避免mysql被打死
事后:redis持久化 RDB + AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
①完全基于内存,它的数据在内存中
②数据结构简单,对数据操作也简单,redis中的数据结构是专门进行设计的
③采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致性能消耗;
④NIO,使用多路I/O复用模型,非阻塞IO;(再去加深印象)
我们可通过在单机开多个redis实例
集群部署(redis cluster),并且是主从同步,读写分离。redis cluster 支持N个redis master node,每个master node都可以挂载多个slave node.这样redis就可以横向扩容。如果你要支持更大数量的缓存,那就横向扩容更多的master节点,每个master节点就能存放更多的数据了。
持久化是redis高可用比较重要的一个环节,因为redis的数据在内存中,所以必须要持久化。我了解到的持久化有两种方式:
AOF:对每条写入命令作为日志,以append-only的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址开销,所以很快,像MySQL中的binlog。
缺点:一样的数据,AOF文件比RDB还要大。
RDB:对redis中的数据做周期性的持久化。
优点:rdb对redis的性能影响非常小,因为在同步数据的时候它只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比aof快。
缺点:RDB都是快照文件,都是默认5分钟甚至更久的时间才会生成一次,这就意味着这次同步到下次同步这5分钟的数据都很有可能全部丢失。AOF则最多丢失1秒的数据。还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒。
哨兵集群sentinel
哨兵必须使用三个实例去保证自己的健壮性,哨兵+主从不能保证数据不丢失,但是可以保证集群高可用。(M1所在的机器挂了,哨兵还有两个,两个人一看他不是挂了嘛,那我们就选举一个出来执行故障转移不就好啦)
哨兵组件功能:
①集群监控:负责监控master and slave进程是否正常工作
②消息通知:如果某个redis实列有故障,那么哨兵负责发送消息作为报警通知给管理员
③故障转移:如果 master node挂掉了,会自动转移到slave node上
④配置中心:如果故障发生转移了,通知client客户端新的master地址
这个我先说一下为什么会用到主从同步这样的模式,前面提到单机的QPS是有上限的,而且redis的特性就是支撑必须读高并发的,如果一台机器又读又写肯定是不行的。因此这个时候我们可以让这个master去写,数据同步给slave,他们都拿去读,master就分发掉了大量请求,而且扩容的时候还可以轻松实现水平扩容。那么回到正题,怎么扩容呐?
当启动一台slave的时候,它会发送一个psync命令给master,如果这个slave是第一次连接到master,它会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件 事情就是写进本地磁盘,然后加载进内存。然后master会把内存里面的那些缓存的那些重新命名都发给slave。
传输过程中有什么网络问题的,会自动重连的,并且连接之后会把缺少的数据补上的。
redis的过期策略:定期删除+惰性删除
定期删除:比如默认100s就随机抽一些设置了过期时间的key,去检查是否过期,过期就删了。
惰性删除:等来查询我就看你过期没,过期就删了并且不返回,没过期就该怎样就怎样。
-----内存淘汰机制
LRU(最近最少使用)
class LRUCache<K,V> extends LinkedHashMap<K,V>{ private final int CACHE_SIZE; public LRUCache(int cacheSize){ //true 表示让LinkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部 super((int)Math.ceil(cacheSize / 0.75) + 1,0.75f,true); CACHE_SIZE = cacheSize; } @Override protected boolean removeEldestEntry(Map.Entry<K,V> eldest){ //当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据 return size() > CACHE_SIZE; } }
某个时刻,多个系统都去更新某个key。可以基于Zookeeper实现分布式锁。每个系统通过zookeeper获取分布式锁,确保同一时间,只能有一个系统实列在操作某个key,别人都不允许读和写。
我要写入缓存的数据都是从数据库里查出来的,然后都得写入数据库中,写入数据库的时候必须保存一个时间戳,从数据库查出来的时候,时间戳也查出来。每次要写之前,先判断一下当前这个value的时间戳是否比缓存里的value的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
redis内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以redis才叫做单线程模型。它采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应事件处理器进行处理。多个socket可能会并发产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,会将socket产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
文件事件处理器结构:
①多个socket
②IO多路复用程序
③文件事件分派器
④事件处理器
缓存+数据库读写模式
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据放入缓存,同时返回相应。
更新的时候,先更新数据库,然后再删除缓存
因为很多时候,在复杂点的缓存场景中,缓存不单单是从数据库中直接取出来的值。比如更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
更新缓存开销大,一般我们是用到缓存才去算缓存。