Redis详解

redis
一.redis的六种淘汰策略
1.volatile-lru:从已设置过期时间的数据中随机挑选最近使用最少的多个key进行淘汰
2.volatile-ttl:从已设置过期时间的数据中选择即将过期的数据进行淘汰
3.volatile-random:从已经设置过期时间的数据中随机淘汰数据
4.allkeys-lru:从数据中选择最近使用最少的数据淘汰
5.allkeys-random:从数据中任意选择数据进行淘汰
6.noeviction:不进行删除,达到最大内存时,直接返回错误信息

二.什么是redis为什么使用redis
redis:是一种高性能缓存数据库
特点:1.基于内存,性能高效
2.支持分布式,理论上可以无限扩展
3.key-value存储方式
4.可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
5.C/S通讯模型
6.单线程单进程模型
7.丰富的数据类型【value(String),Set(无序),Zset(有序),list,Hash】
8.操作具有原子性
9.持久化
10.高并发读写
11.支持lua脚本

三.各个数据类型的使用场景
String:
使用方法:set key value 设置制定key 的值
get key 获取制定key 的值
SETEX key seconds value 将值关联到key 并设置key的过期时间为seconds
使用场景:1,会话缓存
当用户登陆后使用redis缓存用户的session,每次用户查询登陆信息时从redis中获取
2,计数器
1>比如登陆系统会限制密码错误次数,当一个用户在一段时间内连续输入密码错误后,需要一段时间后才能登陆,把username作为key,错误次数作为value,同时设置过期时间。

2> 手机验证码限收短信次数

3> 统计其他计数

3,定时器
redis的key设置过期时间,基于次特性设置一个定时器

4,对象
把对象序列化后使用redis保存对象,然后在获取对象信息时反序列化value

5,分布式锁
redis提供了setnx()即SET NOT IF EXIST,只有在key不存在的时候才能set成功,这就意味着同一时间有多个请求只有一个请求能保存,
Hash:
Redis hash 是一个键值(key=>value)对集合,即编程语言中的Map类型.
Redis hash 是一个 string 类型的 field 和 value 的映射表.
使用方法:
HSET key field value 将哈希表 key 中的字段 field 的值设为 value 。HGET key field获取存储在哈希表中指定字段的值。HKEYS key获取所有哈希表中的字段HMSET key field1 value1 [field2 value2 ]同时将多个 field-value (域-值)对设置到哈希表 key 中。
使用场景:
hash 特别适合用于存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值(Memcached中需要取出整个字符串反序列化成对象修改完再序列化存回去)

List:
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
使用方法:
LPUSHX key value 将一个值插入到已存在的列表头部 LPUSH key value1 [value2]将一个或多个值插入到列表头部LPOP key移出并获取列表的第一个元素BLPOP key1 [key2 ] timeout移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
使用场景:
1.消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的"抢"列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
2.类目/文章/活动等列表
最常见的就是各个系统的首页数据,包括电商系统的商品类目,拼团活动列表,博客园的首页文章列表等
3.其他
根据push和pop的方式不同,有以下组合方式
lpush + lpop = Stack(栈)lpush + rpop = Queue(队列)lpush + ltrim = Capped Collection(有限集合)lpush + brpop = Message Queue(消息队列)lpush + rpop = Queue(队列)lpush + ltrim = Capped Collection(有限集合)lpush + brpop = Message Queue(消息队列)

SET:
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

使用方法:
SADD key member1 [member2] 向集合添加一个或多个成员 SDIFF key1 [key2]返回给定所有集合的差集SINTER key1 [key2]返回给定所有集合的交集SMEMBERS key返回集合中的所有成员

使用场景:
1.标签(tag)
比如在点餐评价系统中,用户给某商家评价,商家会有多个评价标签,但是不会重复的,如果100万人给某商家评价打了标签,如果使用MySQL数据库获取大数据量去重后的评价标签,会影响数据库的性能和系统的并发量.

2.相同点/异同点
利用交集、并集、差集等操作,可以计算两个人的共同喜好,全部的喜好,自己独有的喜好,共同好友等功能。

zset(sorted set:有序集合)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
使用方法:
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数 ZCARD key获取有序集合的成员数ZREM key member [member ...]移除有序集合中的一个或多个成员
使用场景:
1.排行榜
例如博客园需要对用户发表的文章做排行榜,榜单的维度可能是多个方面的:按照时间、按照点赞数、按照热度,浏览数等
四.使用redis出现的问题
1.缓存一致性问题
解决方法:1.先更新DB,后更新缓存
缺点:并发写会造成DB和缓存数据不一致

2.先更新缓存,后更新DB
缺点:并发写会造成DB和缓存数据不一致

3.先删除缓存,后更新DB
缺点:因为删除是天然幂等的,所以并发写不会数据不一致,但是并发读写还是会存在不一致,
解决:可以通过异步双删或者给数据设置过期时间来解决
异步双删:通过两次删除来解决并发读造成的脏数据
1.cache.delete
2.db.update
3.asynchronousCache.delete

4.先更新DB,后删除缓存(业界推荐方案)
为什么是删除缓存而不是更新?
答:因为在我们删除缓存后,只有在其真正使用到这个数据的时候,才会将其写入缓存,因此我们就不用每次都对缓存进行更新操作,从而保证效率。
假设缓存刚好到期失效时,读请求从db中读取数据,写请求更新完数据后再失效缓存后,读请求将旧数据存入到缓存中,这种情况也会导致脏数据的问题。实际上这种情况发生的概率很低,要发生这种情况的前提条件是写数据库要先于读数据库完成,一般而言读数据库相比于写数据库要耗时更短,这种前提条件成立的概率很低。针对这种情况,也可以采用上面所说的异步双删策略以及过期失效的方式来避免。

如果缓存删除失败的话也会产生问题,借助消息队列的消息重试机制来保证我们一定能够成功删除缓存,从而确保缓存的一致性。引入消息队列后可能会因为消息的处理导致一定程度的延迟,从而引起短期内的消息不一致,引入消息队列后导致问题整体复杂化
2.缓存雪崩
产生原因:缓存中没有数据,导致大量访问数据库,或当大量key同时过期,请求全部访问数据库造成数据库崩溃。
解决方法:
方案一:解决这种场景的可以将失效的时间由固定值+随机值来构成。
方案二:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上
方案三:是限流,在大流量来时缩紧。或者使用令牌桶,漏桶算法。

方案四:是二级缓存,比如在缓存服务的基础上加上本地缓存,在缓存服务失效的情况下查询本地缓存而不是数据库
3.缓存穿透
产生原因:用户或恶意查询一个数据库和缓存都不存在的数据
解决方法:
方案一:采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据就会被这个bitmap拦截掉,从而避免对底层存储系统的查询压力。
方案二:如果一个查询返回的结果是空(不管是数据不存在,还是系统故障)我们仍然将这个空结果缓存,但是过期时间很短,最长不超过五分钟,这样查询就能从缓存中获取到值,就不会在访问数据库,避免大量查询数据库。
五.缓存预热
系统上线后,将相关的缓存数据直接加载到缓存,这样就能避免在用户访问时先查数据库,然后再将数据缓存的问题
解决思路:
方案一:直接写个缓存页面,上线时手动操作
方案二:数据量不大时可在项目上线时自动加载
方案三:定时刷新缓存
六.缓存更新
除了缓存服务器自带的失效策略(Redis默认的有6中策略可供选择)外,我们还可以根据业务自定义缓存淘汰,常见的策略有两种
第一种:定时清理过期的缓存
第二种:当用户访问时,先判断缓存有没有过期,如果过期从数据库查询然后更新缓存
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

七.缓存降级
丢卒保帅,舍弃次要的功能,保证核心业务的正常使用,有的可降级(例如:日志),有的不可降级(例如:购物车,支付)

八.缓存热点key
使用缓存+过期时间的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:

危害一:当前 key 是一个热点 key( 可能对应应用的热卖商品、热点新闻、热点评论等),并发量非常大。
危害二:重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。
解决方法:
方法一:减少重建缓存的次数
方法二:数据尽可能一致
方法三:较少的潜在威胁
1)互斥锁 (mutex key)此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可
伪代码:
String get(String key) {
//从redis中获取key
String value = redis.get(key);
//如果value为空则开始重构缓存
if (value == null) {
//只允许一个线程重构缓存,使用nx,并设置过期时间ex
String mutexKey = "mutex:key" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
//从数据源获取数据
value = db.get(key);
//回写redis并设置过期时间
redis.set(key, value, timeout);
//删除mutexKey
redis.del(mutexKey);
} else {
//其他线程睡眠50秒再重试
Thread.sleep(50);
get(key);
}
}
return value;
}
从 Redis 获取数据,如果值不为空,则直接返回值。
如果 set(nx 和 ex) 结果为 true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。
如果 setnx(nx 和 ex) 结果为 false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间 (例如这里是 50 毫秒,取决于构建缓存的速度 ) 后,重新执行函数,直到获取到数据。
2)永远不过期
永远不过期”包含两层意思:
从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
从实战看,此方法有效杜绝了热点 key 产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。下面代码使用 Redis 进行模拟:

String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
//逻辑过期时间
final Long logicTimeout = v.getLogicTimeout();

//如果逻辑时间小于当前时间,开始重建缓存
if (logicTimeout <= System.currentTimeMillis()) {
final String mutexKey = "mutex:key" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
//重建缓存
threadPool.execute(new Runnable() {
@Override
public void run() {
String dbValue = db.get(key);
redis.set(key, (dbValue, newLogicTimeout));
redis.del(mutexKey);
}
});
}
}
return value;
}
作为一个并发量较大的应用,在使用缓存时有三个目标:第一,加快用户访问速度,提高用户体验。第二,降低后端负载,减少潜在的风险,保证系统平稳。第三,保证数据“尽可能”及时更新。下面将按照这三个维度对上述两种解决方案进行分析。
互斥锁(mutexkey):这种方案思路比较简单,但是存在一定的隐患,如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,但是这种方法能够较好的降低后端存储负载并在一致性上做的比较好。

” 永远不过期 “:这种方案由于没有设置真正的过期时间,实际上已经不存在热点 key 产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
简单分布式锁:
优点:1.思路简单
2.保证一致性

缺点:1.代码复杂度大
2.存在死锁风险
3.存在线程阻塞风险
永不过期:
优点:基本杜绝热点key问题

缺点:1.不保证一致性
2.逻辑过期时间增大代码维护成本和内存成本

九.Redis过期策略
Redis的过期策略
通常Redis keys创建时没有设置相关过期时间,他们会一直存在,除非使用显示的命令移除,例如,使用DEL命令。
EXPIRE一类命令能关联到一个有额外内存开销的key。当key执行过期操作时,Redis会确保按照规定时间删除他们。
key的过期时间和永久有效性可以通过EXPIRE和PERSIST命令(或者其他相关命令)来进行更新或者删除过期时间。

Redis keys过期有两种方式:定期删除和惰性删除。

1.定期删除
定时删除,用一个定时器来负责监视key,当key过期则自动删除key。
虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,会影响redis的性能.

2.惰性删除
客户端尝试访问key时,key会被发现并主动的过期. 但是这样是不够的,因为有些过期的keys,永远不会访问他们,那么他们就永远不会被删除,而占用内存,导致redis内存被过期的key占用.

3.定期删除+惰性删除
redis默认每100ms检查一次,是否有过期的key,有过期key则删除。需要说明的是,redis不是每100ms将所有的key检查一次,而是随机抽取20个keys进行过期检查,同时删除已经过期的keys,如果有多于25%的keys过期,重复抽取。直到过期的keys的百分比低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys

那么问题来了,采用定期删除+惰性删除就能保证过期的key会全部删除掉么?

posted on 2022-01-25 14:56  木_-_木  阅读(199)  评论(0编辑  收藏  举报