一、什么是Redis
Redis 是开源免费的,遵守BSD协议,是一个高性能的key-value非关系型数据库.
二、Redis为什么快
1、纯内存操作
2、单线程操作:避免了上下文的切换,也就不存在线程之间的资源竞争。虽然大多数理论认为CPU不是瓶颈,网络与带宽才是。但是实际的测试中结果并不是如此。
3、采用了非阻塞I/O多路复用机制:多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了事件,不在I/O上浪费过多的时间。
关于多路复用机制的案例解释:假如有一家快递店,负责同城快送服务。因为资金限制,雇佣了一批快递员,发现资金不够了,只够买一辆车送快递。
经营方式一
客户每送来一份快递,小曲就让一个快递员盯着,然后快递员开车去送快递。慢慢的小曲就发现了这种经营方式存在下述问题:
-
时间都花在了抢车上了,大部分快递员都处在闲置状态,抢到车才能去送快递。
-
随着快递的增多,快递员也越来越多,小曲发现快递店里越来越挤,没办法雇佣新的快递员了。
-
快递员之间的协调很花时间。
经营方式二
小曲只雇佣一个快递员。当客户送来快递,小曲按送达地点标注好,依次放在一个地方。最后,让快递员依次去取快递,一次拿一个,再开着车去送快递,送好了就回来拿下一个快递。上述两种经营方式对比,很明显第二种效率更高。
在上述比喻中:
-
每个快递员→每个线程
-
每个快递→每个 Socket(I/O 流)
-
快递的送达地点→Socket 的不同状态
-
客户送快递请求→来自客户端的请求
-
小曲的经营方式→服务端运行的代码
-
一辆车→CPU 的核数
于是有了如下结论:
-
经营方式一就是传统的并发模型,每个 I/O 流(快递)都有一个新的线程(快递员)管理。
-
经营方式二就是 I/O 多路复用。只有单个线程(一个快递员),通过跟踪每个 I/O 流的状态(每个快递的送达地点),来管理多个 I/O 流。
三、Redis可以存储的数据类型
string,List,Hash,Set,Sorted Set
四、Redis事务
redis中的事务与传统关系型数据库(如mysql)的事务是不同的
redis中的事务是一组命令的集合,事务与命令都是最小执行单位,原理是先将属于一个事务的命令发送给Redis,然后Redis一次执行这些命令。
redis的事务可以保证一个事务内的命令一次执行而不被其他命令插入影响。
如果事务块中某一条命令出错,关系型数据库的事务会执行回滚,而redis不会执行回滚,而是会继续执行后续的命令。因为redis的事务没有关系型数据库的回滚(rollback)功能。因此需要开发者在事务执行出错时自己处理。
redis事务(Transaction)命令
1.watch
用于监视一个或多个key,如果在事务执行之前这个或(这些)key被其他命令所改动,事务将被中断。
2.unwatch
用于取消watch命令对所有key的监视。
3.multi
用于标记一个事务块的开始,之后的所有命令都存放在队列,等遇到exec命令再执行。
4.exec
用于执行事务块内所有的命令,如果命令被中断,返回false
五、Redis缓存雪崩,击穿与穿透以及解决方案
1、穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。解决办法有两个:
1)查询数据库不存在,则在redis中存一份Null值,避免访问Mysql
2)采用布隆过滤器,将List数据装载入布隆过滤器中,访问经过布隆过滤器,存在才可以往db中查询。于是在内存中就可以拦截恶意请求。
2、雪崩
雪崩指的是多个key查询并且出现高并发,缓存中失效或者查不到,然后都去db查询,从而导致db压力突然飙升,从而崩溃。
出现原因: 1 key同时失效
2 redis本身崩溃了
方案:
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(跟击穿的第一个方案类似,但是这样是避免不了其它key去查数据库,只能减少查询的次数)
可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
不同的key,设置不同的过期时间,具体值可以根据业务决定,让缓存失效的时间点尽量均匀
做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。(这种方式复杂点)
击穿: 指的是单个key在缓存中查不到,去数据库查询,这样如果数据量不大或者并发不大的话是没有什么问题的。
如果数据库数据量大并且是高并发的情况下那么就可能会造成数据库压力过大而崩溃
注意: 这里指的是单个key发生高并发!!!
解决方案: 1) 通过synchronized+双重检查机制:某个key只让一个线程查询,阻塞其它线程
缺点: 会阻塞其它线程
击穿: 指的是单个key在缓存中查不到,去数据库查询,这样如果数据量不大或者并发不大的话是没有什么问题的。
如果数据库数据量大并且是高并发的情况下那么就可能会造成数据库压力过大而崩溃
注意: 这里指的是单个key发生高并发
解决方案:1) 通过synchronized+双重检查机制:某个key只让一个线程查询,阻塞其它线程
缺点: 会阻塞其它线程
2) 设置value永不过期
这种方式可以说是最可靠的,最安全的但是占空间,内存消耗大,并且不能保持数据最新 这个需要根据具体的业务逻辑来做
个人觉得如果要保持数据最新不放这么试试,仅供参考:
起个定时任务或者利用TimerTask 做定时,每个一段时间多这些值进行数据库查询更新一次缓存,当然前提时不会给数据库造成压力过大(这个很重要)
3) 使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。