更多Redis入门系列文章,参见Redis入门-大纲
缓存
为什么使用redis做缓存
性能和并发两个角度:
1)我们在碰到需要执行耗时特别久,且结果不频繁变动的SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。
2)在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库。
redis vs. memCache vs. ehCache
三者都可以用来做缓存
Memcache
Memcache可以利用多核优势,单实例吞吐量极高,可以达到几十万QPS,适用于最大程度扛量
只支持简单的key/value数据结构,不像Redis可以支持丰富的数据类型。
无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失
Redis
支持多种数据结构,如string,list,dict,set,zset,hyperloglog
单线程请求,所有命令串行执行,并发情况下不需要考虑数据一致性问题。
支持持久化操作,可以进行aof及rdb数据持久化到磁盘,从而进行数据备份或数据恢复等操作,较好的防止数据丢失的手段。
支持通过Replication进行数据复制,通过master-slave机制,可以实时进行数据的同步复制,支持多级复制和增量复制.
Ehcache
Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。与Redis明显不同,它与java程序是绑在一起的,java程序活着,它就活着。譬如,写一个独立程序放数据,再写一个独立程序拿数据,那么是拿不到数据的。只能在独立程序中才能拿到数据。ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便.
redis和数据库双写一致性问题
一致性问题的产生
如果先写库,再删除缓存,库写完了线程崩了,没有删除掉缓存,缓存存活时间内数据不一致。暂无解决方案。
如果先删缓存,再写库,删完缓存线程崩了,没有写库。无一致性问题。
如果先删缓存,再写库,删完缓存,写库之前有新请求,读取了修改之前的数据存入缓存,之后库写完,则缓存中存的旧数据,出现一致性问题。解决方法是做一个消息队列,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同对象在更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。如果数据更新很频繁,导致队列中挤压了大量的更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库,建议还是加机器吧。。。
先删缓存 @CacheEvict(beforeInvocation)
如何应对缓存穿透和缓存雪崩问题
缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
解决方案:
(一)利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
(二)采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
(三)提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。
(一)给缓存的失效时间,加上一个随机值,避免集体失效。
(二)使用互斥锁,但是该方案吞吐量明显下降了,不推荐。
(三)双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点
I 从缓存A读数据库,有则直接返回
II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
III 更新线程同时更新缓存A和缓存B。
消息队列
Redis提供了两种方式来作消息队列。一个是使用生产者消费模式模式,另外一个方法就是发布订阅者模式。前者会让一个或者多个客户端监听消息队列,一旦消息到达,消费者马上消费,谁先抢到算谁的,如果队列里没有消息,则消费者继续监听。后者也是一个或多个客户端订阅消息频道,只要发布者发布消息,所有订阅者都能收到消息,订阅者都是平等的。
生产者消费模式模式
redis的队列实际在代码逻辑中不需要由我们自己实现,因此一个所谓的 RedisMQ 对象实际是一个 redis key以及对其操作的一些封装。
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。如果不用sleep,list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
需要注意的是,Redis不适合拿来当消息队列,一些基本的要求:顺序保证,消息至少到达一次,持久化等等特性都完全没有保证。如果是内部系统拿来跑一些异步任务并且对可用性和性能要求都不高的情况下,不失是一种省事方便的选择。如果场景是正儿八经的业务,还是选真正是 mq 比较好。
redis 4.2版本中加入了disque module作为redis内置的消息队列,部署、协议等方面都跟redis非常类似,并且支持集群,延迟消息等等。
发布订阅者模式
订阅,取消订阅和发布实现了发布/订阅消息范式,发布者不是计划发送消息给特定的订阅者。而是发布的消息分到不同的频道,不需要知道什么样的订阅者订阅。订阅者对一个或多个频道感兴趣,只需接收感兴趣的消息,不需要知道什么样的发布者发布的。
这是一种基于非持久化的消息机制,消息发布者和订阅者必须同时在线,否则一旦消息订阅者由于各种异常情况而被迫断开连接,在其重新连接后,其离线期间的消息是无法被重新通知的(即发即弃)。
Redis中的消息可以提供两种不同的功能。一类是基于Channel的消息,这一类消息和Redis中存储的Keys没有太多关联,也就是说即使不在Redis中存储任何Keys信息,这类消息也可以独立使用。另一类消息可以对(也可以不对)Redis中存储的Keys信息的变化事件进行通知,可以用来向订阅者通知Redis中符合订阅条件的Keys的各种事件。
注意:subscribe的时候线程是会被阻塞的。且在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。
数据库
Redis 的数据结构很丰富,精心设计地话,能满足很多应用场景。但是它并不通用,使用场景是有限的。性能、成本、可靠性,最终是一个权衡的问题。
对于一些小型系统,例如企业后台,数据量小,读写不频繁时,redis可以大大提高响应速度。
对于大型线上项目,比如你每日活跃用户是1万人,但是你那台redis里面已经积累了50万人了(这个比例很正常),那么每次redis启动,就需要把50万load内存,每次redis备份,又需要把50万dump到磁盘,这时用redis作为数据库就不靠谱。