Java程序员从笨鸟到菜鸟(五十三) 分布式之 Redis

作者 明割
邮箱 [email protected]

原文传送门https://www.cnblogs.com/rjzheng/p/9096228.html 博客讲的非常清晰易懂,非常感谢作者

##目录

##一、为什么使用 Redis

分析:主要是从两个角度去考虑:性能和并发
1.性能
如下图示,在项目过程中会碰到需要执行耗时特别久,且结果变动不频繁的 SQL,就特别适合将运行结果放入缓存,这样后面的请求就去缓存中读取,使得请求能够迅速响应
这里写图片描述

科普时间一瞬间、刹那、一弹指
根据《摩诃僧祗律》记载

一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾。
经过计算:一瞬间为 0.36 秒,一刹那有 0.018 秒,一弹指长达 7.2 秒

2.并发
如下图示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常,这是就需要 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库
这里写图片描述

##二、Redis 缺点
主要有四个问题

  1. 缓存和数据库双写一致性问题
  2. 缓存雪崩问题
  3. 缓存击穿问题
  4. 缓存的并发竞争问题

##三、单线程 Redis

分析:其实是对 Redis 内部机制的一个考察
主要有以下三点

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 采用非阻塞 I/O 多路复用机制

I/O 多路复用机制
例如小李在 S 城开了一家快递店,负责同城快递业务,雇佣了一批快递员,然后因为资金限制,只够买一辆车送快递。
经营方式一:
客户每送一份快递,就让快递员盯着,然后开车去送快递,慢慢就发现了存在的问题:

  • 几十个快递员基本上时间都花在了等车上,大部分快递员处于闲置状态
  • 随着快递的增多,快递员也越来越多,小李发现快递店越来越拥挤,没办法只有雇佣新的快递员送快递
  • 快递员之间协调车辆比较花时间

经过后续分析,就有了第二种经营方式
**方式二:**只雇佣一个快递员,然后客户送来的快递,按送达地点标注好,依次放在一个地方,最后那个快递员一次去取快递,一次拿一个,然后开车去送。
相比较方式一而言,方式二的效率更高

  1. 每个快递员:每个线程
  2. 每个快递:每个 Socket(I/O)
  3. 快递的送达地点:Socket 的不同状态
  4. 客户快递请求:来自客户端请求
  5. 经营方式:服务端策略
  6. 一辆车:CPU 的核数
    结论:
    1、经营方式一就是传统的并发模型,每个 I/O 流(快递)都有一个新的线程(快递员)管理
    2、经营方式二就是 I/O 多路复用。只有单个线程(快递员),跟踪每个 I/O 流的状态(快递送达地点),来管理多个 I/O 流
    下图类比到真实的 Redis 线程模型:
    这里写图片描述

##四、Redis 数据类型以及应用场景
一共五种数据类型
1、String
最常规的 set、get 操作,value 可以使 String 也可以是数字,一般用于一些复杂的计数功能的缓存
2、hash
这里 value 存放的是结构化的对象,比较方便的就是操作其中某个字段,例如单点登录,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果
3、list
可以做简单的消息队列的功能;可以利用 lrange 命令,做基于 Redis 的人也功能,性能极佳,用户体验好
4、set
set 堆放的是一堆不重复值的集合,可以做全局去重的功能,为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了
5、sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列,可以做排行榜应用,取 TOP N 操作

##五、过期策略以及内存淘汰机制

分析:例如 Redis 只能存 5G 数据,但是你写了 10G,那会删除 5G 数据。怎样删除,数据已经设置了过期时间,但是时间到了,内存占用率还是比较高

针对这个问题,Redis 采用定期删除+惰性删除策略
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略

定期删除+惰性删除是如何工作的?
定期删除:redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除
惰性删除:也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除

采用定期删除+惰性删除就没有其他问题?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制

在 redis.conf 中有一行配置

# maxmemory-policy volatile-lru

就是配置内存淘汰策略:

  1. noeviction;当内存不足以容纳新写入的数据时,新写入的操作会报错。几乎不用
  2. allkeys-lru:当内存不足以容纳新写入的数据时,在键空间中,移除最近最少使用的 key。推荐使用
  3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。几乎不用
  4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。几乎不用
  5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。几乎不用
  6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除.几乎不用

##六、数据库双写一致性的问题
分析:一致性问题是分布式常见问题,还可以再分为最终一致性强一致性。数据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。

解决:采取正确更新策略,先更新数据再刷新缓存,因为可能存在删除缓存失败的问题,可以提供一个补偿措施,如消息队列,也可参考:《分布式之数据库和缓存双写一致性方案解析》

##七、如何应对缓存击穿和缓存雪崩的问题

缓存穿透:黑客故意去请求缓存中不存在的数据,导致所有的请求都到数据库上,从而数据库连接异常
解决方案

  1. 利用互斥锁:缓存失效的时候,先获得锁,得到锁了,再去请求数据库,没得到锁,则休眠一段时间重试
  2. 采用异步更新策略:无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作
  3. 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回

缓存雪崩:缓存同一时间大面积失效,核实后又来了一波请求,结果请求都到数据库上,从而导致数据库连接异常
解决方案

  1. 给缓存失效的时间,加上一个随机值,避免集体失效
  2. 使用互斥所,但是会影响系统吞吐量
  3. 双缓存,有两个缓存,缓存 A 和缓存 B,缓存 A 的失效时间为 20 分钟,缓存 B 不这是失效时间:
    1、从缓存 A 读取数据库,有则直接返回
    2、A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程
    3、更新线程同时更新缓存 A 和缓存 B

##八、如何解决 Redis 的并发竞争问题

分析:这个问题大致是同时有多个子系统去 set 一个 key,大部分解决策略是采用 Redis 的事务机制,但是会有一个问题:因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上

解决方案

  1. 如果对这个 key,不要求顺序,这种情况下可以准备一个分布锁,大家去抢锁,抢到锁就做 set 操作,比较简单
  2. 如果对这个 key,要求顺序,假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳,将 set 方法变成串行访问

猜你喜欢

转载自blog.csdn.net/u013090299/article/details/82151722
今日推荐