1)redis的线程池配置参数与LRU内存淘汰策略 2)redis缓存的使用以及使用注意点 3)redis分布式锁的实现与zoopkeep实现锁的一些区别。
目录
- 一 Redis其他参数
- 1-1 redis数据库连接池参数该如何配置?
- 1-2 redis的内存淘汰之LRU优化策略
- 二 缓存相关
- 2-1 redis缓存的作用/使用问题
- 2-2 redis为什么性能这么好,能够满足高并发,高性能系统的需求?
- 问题1:redis性能好的原因?
- 问题2:为什么redis不采用多线程的IO复用机制?
- 问题3:redis的IO多路复用机制实现原理?
- 2-3 缓存使用之双写不一致?
- 2-3-1 经典策略cache aside pattern的介绍
- 问题1:为什么更新数据库时不同时更新缓存而是删除缓存?
- 2-3-2 数据一致性分情况分析
- 2-4 redis缓存使用之雪崩,穿透
- 3 redis的分布式锁
- 3-1 为什么需要分布式锁
- 3-2 redis中使用SET命令实现分布式锁
- 问题:锁的失效时间无法合理评估该如何解决?
- 3-3 RedLock(Redis)算法实现分布式锁
- 3-4 zookeeper实现分布式锁
- 参考资料
一 Redis其他参数
1-1 redis数据库连接池参数该如何配置?
背景:实际开发中,我们通常会使用类似于jedis的工具连接redis,这类工具通常会带有连接池,连接池参数配置对于性能有着较大的影响。
相关参数:
参数 | 说明 | ||
timeout | Jedis的socket timeout(连接超时等待)值,单位毫秒; | ||
maxRedirections | 最大重定向次数;超过设置后,此抛出异常 | ||
MaxTotal(重要) | 连接池的最大并发连接数 | ||
MaxIdle(重要) | 最大空闲连接数,空闲连接超过该值会回收空闲连接,直到空闲连接数达到Mindle个数 | ||
MinIdle(重要) | 连接池中最小的空闲可用连接数,这部分不被回收。可防止流量增量时,连接创建不及时 |
连接池参数配置的目标:在满足连接需求上,尽可能的复用连接池的连接,减少新建连接的数量
案例1:每秒新建的连接数量大于实际连接数量?
解决策略:可能原因是最大空闲连接数目太少,造成连接被反复创建,应该提高最大空闲连接数目
02 jedis以及redis的持久化
redis连接池优化
1-2 redis的内存淘汰之LRU优化策略
常规的LRU实现:双向链表+hashmap
Redis中的LRU实现:Approximate LRU algorithm(近似的LRU实现)
淘汰机制
- 常规实现淘汰:新数据放入到链表头部,老数据从链表尾部淘汰
- Redis淘汰机制:每次从数据库中随机采样k个key,将k个key中最久没有访问的给淘汰掉。为了实现这种机制,redis为每个key添加了24bit的空间作为时间戳从而确定key的最近访问时间。
- k值的设置参数:maxmemory-samples 5,redis会自己维护24bit时间
背后的动机:内存资源对于redis非常宝贵,通过双向链表维护访问顺序,链表节点的额外开销对于redis来说是不可接受的。
近似LRU的优缺点:
优点:以较小的内存占用实现了近似的LRU淘汰效果
缺点:会造成一些热点数码被不合理的淘汰掉
Redis3.0对LRU的优化:
Redis3.0 has some optimizations to the approximate LRU algorithm. The new algorithm maintains a candidate pool (size 16). The data in the pool is sorted according to the access time. The first randomly selected key will be placed in the pool, and then each randomly selected key will only be accessed when the access time is less than the pool. The minimum time will be placed in the pool until the candidate pool is full. When it is full, if there is a new key that needs to be placed, the one with the longest access time (recently accessed) in the pool will be removed. When it needs to be eliminated, the key with the least recent access time (the longest not accessed) is directly selected from the pool and eliminated.
Comparison and analysis of redis cache elimination strategy LRU and LFU
二 缓存相关
2-1 redis缓存的作用/使用问题
问题1:什么时候需要redis缓存?
1)系统要求高性能:提高数据获取的效率,让用户的体验更加好。
原因:redis缓存放在内存中,从内存中获取要比从数据库获取效率更高。
2)系统要求高并发:redis相比仅仅使用mysql能够同时支持更多的访问。
问题2:缓存的使用存在的问题?
1)缓存与数据库双写不一致
2)缓存雪崩,缓存穿透,缓存击穿,缓存预热问题
3)缓存并发竞争
2-2 redis为什么性能这么好,能够满足高并发,高性能系统的需求?
为什么需要多线程IO复用
问题1:redis性能好的原因?
1)redis是基于内存key-value数据库,相比较数据存储在磁盘文件的关系型数据库性能肯定更好
2) redis使用网络套接字读取数据,采用了IO多路复用机制。
3)在实现上,C实现更加贴近系统底层,执行快。
问题2:为什么redis不采用多线程的IO复用机制?
常规意义上的单线程IO复用机制的缺点:
1)缺点1:多核 cpu被白白浪费
2)缺点2:某个事件耗费时间比较长会影响其他事件的处理。
从redis情况分析:
1)针对缺点1,redis在充分利用多核心CPU资源前,网络的带宽以及内存会先变为瓶颈,自然多线程也无必要上(实际很多服务器都是单核)
2)针对缺点2:redis基于内存的操作,绝大部分读写时间的处理都非常快,不太会出现某个key的读取/写入耗费时间比较长阻塞其他线程。
3)单线程自然也有优点,实现比多线程简单,也不需要考虑线程安全问题。
问题3:redis的IO多路复用机制实现原理?
常规意义的单线程IO多路复用的定义:单线程配合selector选择器虽然能够管理多个channel的事件
redis中单路IO复用的实现:⽂件事件处理器 file event handler
1)通过IO多路复用程序(即selector)监听多个socket。
2)当多个socket有读写事件发生,IO多路复用模块将对应的文件事件给放入到队列中
3)文件事件事件分派器每次会从队列中获取一个事件并使用调用对应的事件处理器相应处理
事件处理器类型:连接应答处理器,命令请求处理器,命令回复处理器
2-3 缓存使用之双写不一致?
2-3-1 经典策略cache aside pattern的介绍
cache aside pattern的基本思想:
- 读取数据:先确定数据是否在缓存中,如果不在则从数据库中读并将读取的数据放入到缓存中。
- 更新数据:先更新数据库,然后让缓存失效(删除缓存)
问题1:为什么更新数据库时不同时更新缓存而是删除缓存?
两个原因:
1) 缓存的更新的代价比较高,部分缓存需要联合多个表进行计算,单一表更新就去更新缓存不符合实际
2) 更新的数据表对应的缓存可能是冷数据,冷数据使用时再去读取并放入缓存更加合理。
2-3-2 数据一致性分情况分析
基本思想:结合具体的业务场景进行分析。
情况1:读多写多场景下,如果不要求非常严格的一致性(短时间内的不一致是允许的),比如说商品的库存数量
解决策略:采用cache aside pattern,数据更新时,先更新数据库,再删除缓存,数据读取时,先读取缓存,缓存没有再查询数据库,并将数据放入缓存中。
- 实际开发中我们可以合理设置key的超时时间,来保证缓存中的数据与数据库中数据一致程度。
存在隐患:
- 热点数据更新时需要考虑缓存雪崩,穿透等问题
- 更新线程在进行数据库更新时或者删除缓存失败时,其他线程仍然会读取到旧的值
情况2:极端情况,数据库与缓存要求严格一致。
解决策略:读请求和写请求串⾏化,串到⼀个内存队列⾥去
- 串⾏化可以保证⼀定不会出现不⼀致的情况,但是它也会导致系统的吞吐量⼤幅度降低,⽤⽐
正常情况下多⼏倍的机器去⽀撑线上的⼀个请求
情况3:如何我们先删除缓存然后更新数据库,如何确保缓存数据库的一致性
不一致原因:删除缓存后,线程A更新数据库的这段时间,其他线程比如线程B可能会读取数据数据放入到缓存中。
- 上面不一致会在并发量比较大的情况下出现
网上的解决方案:
基本思想:利用更新数据的id与工作队列将对数据库的更新查询操作串行化
基本流程就是:
1)线程A更新数据时,根据数据的唯⼀标识,将操作路由之后,发送到⼀个 jvm 内部队列中。读取
2)线程B读取相同数据时的时候,如果发现数据不在缓存中,那么把重新读取数据+更新缓存的操作,根据唯⼀标识
路由之后,也发送同⼀个 jvm 内部队列中。(就是让读取的时候不去),然后同步等待缓存中有数据。
存在的问题:
问题1:队列中重复的更新请求是没有意义的,因此需要对队列中的请求进行过滤
问题2:在等待的过程中,读取数据的线程存在请求超时问题,这个问题需要在上线前进行压力测试,去获取队列中请求堆积程度,估算请求等待时间,避免请求超时问题,必要的话加大机器,每个机器负责一部分数据的更新请求处理
问题3:多服务器实例部署,相同数据请求的更新操作应该路由到同一实例服务器上进行
问题4:热点商品的路由问题,负责热点商品数据更新的机器其负载可能高于一般数据的更新
缓存的一致性问题
2-4 redis缓存使用之雪崩,穿透
缓存雪崩发生场景:缓存机器意外发⽣了全盘宕机
缓存雪崩的解决策略:
事前:
1)系统设计时保证缓存系统的高可用,主从结构+哨兵机制或者采用多主从架构的集群模式,避免缓存的全面崩溃
2)采用多级缓存架构:⽤户发送⼀个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果
ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写⼊ ehcache 和 redis 中
事件发生:
1)缓存雪崩发生时, hystrix 限流&服务降级,避免 MySQL的崩溃
事件发生后:可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,⾛降级!可以返回⼀些默认的值,或者友情提示,或者空⽩的值.
3)利用好redis持久化机制,在限流降级后,服务重启迅速恢复数据。
- EhCache 是一个纯 Java 的进程内缓存框架,缓存数据可以放入磁盘与内存中,这个缓存可以辅助NoSQL数据库提供缓存功能。
缓存穿透
3 redis的分布式锁
3-1 为什么需要分布式锁
单机锁:同一个进程内多个线程内操作共享数据时,为了数据的安全性,需要借助锁实现
分布式锁:微服务架构,应用会部署多台机器的多个进程,多个进程如果需要修改共同数据(MySQL 中的同一行记录时)则需要分布式锁
分布式锁的关键:
1)互斥(只能有⼀个客户端获取锁)
2)不能死锁
3)容错(只要⼤部分 redis 节点创建了这把锁就可以)
3-2 redis中使用SET命令实现分布式锁
redis中锁
在redis中提供了2种基本的锁:
1)一个是基于乐观锁思想的监视锁,使用watch命令添加监控锁,可以确保多个客户端执行同样的修改,但只有一个能够修改有效。但监视锁只能监视key是否变化。
2)另外一个则是基于悲观锁思想的setnx锁,这个锁也是通过key实现的,全称为set if not exist,只有key不存在才能设置key,通过这个命令来实现锁机制。
多个客户端遵循以下完整流程
step1: set 锁的key 锁的value EX 超时事件 NX // NX: Not eXists的缩写
step2: 操作共享资源
step3: 使用Lua脚本判断当前锁是否是自己设置的,然后删除这个锁。
上面流程几个关键点:
关键1: 超时时间的设置:加锁的时候设置超时时间,避免加锁客户端出现问题而无法释放锁。超时时间需要确保长于资源操作时间
关键2:锁的释放问题:每个客户端加锁时应该设置唯一的value(可以采用随机值)作为标识,删除锁的时候只有value一致的情况下,才能删除,避免自己的锁由于过期,将别人的锁给释放了。
关键3:Lua脚本配合redis的单线程模型保证锁的判断与锁的删除共同执行。
Lua脚本能够让脚本中的命令全部执行,
上面流程存在的问题:1)锁的失效时间需要评估 2)容错性不好,如果在master slave设置了锁,master slave宕机,从服务器还没同步到key,这个时候选为主服务器,别人依旧可以设置锁进行操作。
问题:锁的失效时间无法合理评估该如何解决?
传统策略:过度冗余
Redisson提供的策略:加锁是先设计一个过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期但是当前共享资源的操作还未完成,则自动对锁进行续期,延长过期时间。
3-3 RedLock(Redis)算法实现分布式锁
ReadLock锁使用前提:主从结构的中要保证独立的master实例(主库)要有5个
- 这里并不要求每个主库都拖了一堆从库,只是要求要有5个主库
ReadLock加锁的流程:
step1:加锁客户端获取当前时间戳后,然后依次向5个redis实例发出加锁请求(命令与3-2一致)。
加锁过程中如何加锁失败(请求超时,锁被其他人持有)则继续下一个实例请求,直到请求完所有redis主库实例。
step2:如果客户端从超过一半的redis实例中加锁成功,那么再次获取时间戳T2,并将加锁的过期时间与T2-T1对比
step3:T2-T1 < 锁的过期时间则加锁成功,客户端能够操作资源,其余情况则加锁失败。
step4:操作资源完成后或者加锁失败则通过Lua脚本对所有节点发出请求要求删除锁
关键1:通过多个redis主库实例加锁相比单主库加锁提供了加锁的容错性,
关键2:通过系统提供的时间戳计算加锁的累计耗时,对当前的网络情况进行了探测,从而更好的设置超时时间
关键3:无论加锁是否成功,需要释放锁的时候对于所有实例都发出释放锁的请求
分布式锁面临的NPC问题:网络延迟,垃圾回收,时钟漂移
N:Network Delay
P: Process Pause
C: Clock Drift
- Redis Lock的作者认为ReadLock是能够通过step3即加锁时间计算去检测到NPC问题,避免锁的有效性不合理造成锁的提前失效。
3-4 zookeeper实现分布式锁
zookeeper的提供的功能:配置管理,分布式锁,集群管理
- 具有树形目录结构
zookeeper实现分布式锁的流程:
1)客户端1/2都尝试创建/lock节点(互斥性)
2)客户端1的请求先到达,则加锁成功,客户端2加锁失败
3)客户端1操作共享资源
4)客户端1删除/lock节点,释放锁。
互斥性对比:redis是通过相同的key来保证互斥性,而zoopkeeper则是通过虚拟节点的创建来保证
死锁避免:redis通过设置过期时间让锁失效,zoopkeeper则是通过客户端的心跳机制确保客户端持有锁.
zookeeper分布式锁不安全的实例:
- 极端情况下分布式锁并非安全的,这个不安全性主要由于分布式系统中的NPC问题
参考资料