1 什么是redis
redis是nosql(也是个微小的map) 单线程,然而可解决1秒10w的并发(数据都在内存中)
应用java对redis进行操作相似jdbc接口标准对mysql,有各类实现他的实现类,咱们罕用的是druid
其中对redis,咱们通常用Jedis(也为咱们提供了连接池JedisPool)
在redis中,key就是byte[](string)
redis的数据结构(value): String、list、set、orderset、hash
2 redis的应用
先装置好redis,而后运行,在pom文件中引入依赖,在要应用redis缓存的类的mapper.xml文件配置redis的全限定名。引入redis的redis.properties文件(如果要更改配置就能够应用)
利用场景:
String :
1存储json类型对象,
2计数器,
3优酷视频点赞等
list(双向链表)
1能够应用redis的list模仿队列,堆,栈
2敌人圈点赞(一条朋友圈内容语句,若干点赞语句)
规定:朋友圈内容的格局:
1,内容: userpost:x content来存储;
2,点赞: postgood list来存储;(把相应头像取出来显示)
hash(hashmap)
1保留对象
2分组
3 string与hash的数据差异
在网路传输时候,必须要进行进行序列化,才能够进行网路传输,那么在应用string类型的类型的时候须要进行相干序列化,
hash也是要进行相干的系列化,所以会存在很多序列化,在存储的时候hash是能够存储的更加丰盛,然而在反序列化的时候,string的反序列化绝对较低,而hash的序列化和返序列化是绝对hash类更加简单,所以看业务场景,如果是数据常常批改的那种,为了性能能够应用string,如果是数据不是常常改的那种就能够应用hash,因为hash,存储数据时比拟丰盛,能够存储多种数据类型
4 redis的长久化形式:
能,将内存中的数据异步写入硬盘中,两种形式:RDB(默认)和AOF
RDB长久化原理:通过bgsave命令触发,而后父过程执行fork操作创立子过程,子过程创立RDB文件,依据父过程内存生成长期快照文件,实现后对原有文件进行原子替换(定时一次性将所有数据进行快照生成一份正本存储在硬盘中)
长处:是一个紧凑压缩的二进制文件,Redis加载RDB复原数据远远快于AOF的形式。
毛病:因为每次生成RDB开销较大,非实时长久化,
AOF长久化原理:开启后,Redis每执行一个批改数据的命令,都会把这个命令增加到AOF文件中。
长处:实时长久化。
毛病:所以AOF文件体积逐步变大,须要定期执行重写操作来升高文件体积,加载慢
5 redis单线程为什么这么快
redis是单线程的,然而为什么还是这么快呢,
起因1: 单线程,防止线程之间的竞争
起因2 :是内存中的,应用内存的,能够缩小磁盘的io
起因3:多路复用模型,用了缓冲区的概念,selector模型来进行的
6 redis主挂了怎么操作
redis提供了哨兵模式,当主挂了,能够选举其余的进行代替,哨兵模式的实现原理,就是三个定时工作监控,
6.1 每隔10s,每个S节点(哨兵节点)会向主节点和从节点发送info命令获取最新的拓扑构造
6.2 每隔2s,每个 S节点(哨兵节点) 会向某频道上发送该S节点(哨兵节点)对于主节点的判断以及以后S节点(哨兵节点) 的信息,
同时每个Sentinel节点也会订阅该频道,来理解其余S节点(哨兵节点) 以及它们对主节点的判断(做主观下线根据)
6.3 每隔1s,每个S节点(哨兵节点) 会向主节点、从节点、其余S节点(哨兵节点) 发送一条ping命令做一次心跳检测(心跳检测机制),来确认这些节点以后是否可达
当三次心跳检测之后,就会进行投票,当超过半数以上的时候就会将该节点当做主
7 redis集群
redis集群在3.0当前提供了ruby脚本进行搭建,引入了糙的概念,
Redis集群内节点通过ping/pong音讯实现节点通信,音讯岂但能够流传节点槽信息,还能够流传其余状态如:主从状态、节点故障等。因而故障发现也是通过音讯流传机制实现的,次要环节包含:主观下线(pfail)和主观下线(fail)
主客观下线:
主观下线:集群中每个节点都会定期向其余节点发送ping音讯,接管节点回复pong音讯作为响应。如果通信始终失败,则发送节点会把接管节点标记为主观下线(pfail)状态。
主观下线:超过半数,对该主节点做主观下线
主节点选举出某一主节点作为领导者,来进行故障转移。
故障转移(选举从节点作为新主节点)
8 内存淘汰策略
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么解决须要新写入且须要申请额定空间的数据。
noeviction:当内存不足以包容新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以包容新写入数据时,在键空间中,移除最近起码应用的key。
allkeys-random:当内存不足以包容新写入数据时,在键空间中,随机移除某个key。
volatile-lru:当内存不足以包容新写入数据时,在设置了过期工夫的键空间中,移除最近起码应用的key。
volatile-random:当内存不足以包容新写入数据时,在设置了过期工夫的键空间中,随机移除某个key。
volatile-ttl:当内存不足以包容新写入数据时,在设置了过期工夫的键空间中,有更早过期工夫的key优先移除。
9 REDIS缓存穿透,缓存击穿,缓存雪崩起因+解决方案
粗体
- 缓存穿透:key对应的数据在数据源并不存在,每次针对此key的申请从缓存获取不到,申请都会到数据源,从而可能压垮数据源。比方用一个不存在的用户id获取用户信息,不管缓存还是数据库都没有,若黑客利用此破绽进行攻打可能压垮数据库。
- 缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发申请过去,这些申请发现缓存过期个别都会从后端DB加载数据并回设到缓存,这个时候大并发的申请可能会霎时把后端DB压垮。
- 缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段生效,这样在生效的时候,也会给后端系统(比方DB)带来很大压力。
缓存穿透解决方案
一个肯定不存在缓存及查问不到的数据,因为缓存是不命中时被动写的,并且出于容错思考,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次申请都要到存储层去查问,失去了缓存的意义。
有很多种办法能够无效地解决缓存穿透问题,最常见的则是采纳布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个肯定不存在的数据会被 这个bitmap拦挡掉,从而防止了对底层存储系统的查问压力。另外也有一个更为简略粗犷的办法(咱们采纳的就是这种),如果一个查问返回的数据为空(不论是数据不存在,还是系统故障),咱们依然把这个空后果进行缓存,但它的过期工夫会很短,最长不超过五分钟。
粗犷形式伪代码:
// 伪代码public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } else { // 数据库查问不到,为空 cacheValue = GetProductListFromDB(); if (cacheValue == null) { // 如果发现为空,设置个默认值,也缓存起来 cacheValue = string.Empty; } CacheHelper.Add(cacheKey, cacheValue, cacheTime); return cacheValue; }}
缓存击穿解决方案
key可能会在某些工夫点被超高并发地拜访,是一种十分“热点”的数据。这个时候,须要思考一个问题:缓存被“击穿”的问题。
应用互斥锁(mutex key)
业界比拟罕用的做法,是应用mutex。简略地来说,就是在缓存生效的时候(判断拿进去的值为空),不是立刻去load db,而是先应用缓存工具的某些带胜利操作返回值的操作(比方Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回胜利时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的办法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,能够利用它来实现锁的成果。
public String get(key) { String value = redis.get(key); if (value == null) { //代表缓存值过期 //设置3min的超时,避免del操作失败的时候,下次缓存过期始终不能load db if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置胜利 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex); } else { //这个时候代表同时候的其余线程曾经load db并回设到缓存了,这时候重试获取缓存值即可 sleep(50); get(key); //重试 } } else { return value; } }
memcache代码:
if (memcache.get(key) == null) { // 3 min timeout to avoid mutex holder crash if (memcache.add(key_mutex, 3 * 60 * 1000) == true) { value = db.get(key); memcache.set(key, value); memcache.delete(key_mutex); } else { sleep(50); retry(); } }
其它计划:待各位补充。
缓存雪崩解决方案
与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key。缓存失常从Redis中获取,示意图如下:
缓存生效霎时示意图如下:
缓存生效时的雪崩效应对底层零碎的冲击十分可怕!大多数零碎设计者思考用加锁或者队列的形式保障来保障不会有大量的线程对数据库一次性进行读写,从而防止生效时大量的并发申请落到底层存储系统上。还有一个简略计划就时讲缓存生效工夫扩散开,比方咱们能够在原有的生效工夫根底上减少一个随机值,比方1-5分钟随机,这样每一个缓存的过期工夫的反复率就会升高,就很难引发个体生效的事件。
加锁排队,伪代码如下:
//伪代码public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String lockKey = cacheKey; String cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { synchronized(lockKey) { cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //这里个别是sql查问数据 cacheValue = GetProductListFromDB(); CacheHelper.Add(cacheKey, cacheValue, cacheTime); } } return cacheValue; }}
加锁排队只是为了加重数据库的压力,并没有进步零碎吞吐量。假如在高并发下,缓存重建期间key是锁着的,这是过去1000个申请999个都在阻塞的。同样会导致用户期待超时,这是个治标不治本的办法!
留神:加锁排队的解决形式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因而,在真正的高并发场景下很少应用!
随机值伪代码:
//伪代码public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; //缓存标记 String cacheSign = cacheKey + "_sign"; String sign = CacheHelper.Get(cacheSign); //获取缓存值 String cacheValue = CacheHelper.Get(cacheKey); if (sign != null) { return cacheValue; //未过期,间接返回 } else { CacheHelper.Add(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) -> { //这里个别是 sql查问数据 cacheValue = GetProductListFromDB(); //日期设缓存工夫的2倍,用于脏读 CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2); }); return cacheValue; }}
解释阐明:
- 缓存标记:记录缓存数据是否过期,如果过期会触发告诉另外的线程在后盾去更新理论key的缓存;
- 缓存数据:它的过期工夫比缓存标记的工夫缩短1倍,例:标记缓存工夫30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,理论缓存还能把旧数据返回给调用端,直到另外的线程在后盾更新实现后,才会返回新缓存。
对于缓存解体的解决办法,这里提出了三种计划:应用锁或队列、设置过期标记更新缓存、为key设置不同的缓存生效工夫,还有一种被称为“二级缓存”的解决办法。