Java高并发优化:中间件redis

2023年1月29日10:28:42

1. 高并发系统设计的目标

  • 高性能:性能体现了系统的并行处理能力
  • 高可用:表示系统可以正常服务的时间
  • 高扩展:表示系统的扩展能力,流量高峰时能否在短时间内完成扩容

性能衡量指标
大体上可以分为:吞吐量、响应时间、并发量、秒开率和正确性等
吞吐量:单位时间内执行的请求数量
并发量:并发量指的是系统能够同时处理的请求数量,反映的是系统的负载能力
秒开率:秒开率主要针对的是前端网页或者移动端APP来说的,如果一个前端网页或者APP能够在1秒内很平滑的打开,尤其是首页的加载
正确性:

Java高并发优化:中间件redis

2. 高并发中的问题

日常分享模块:请求数据量大,网络IO操作

解决方案:

  1. 上拉加载机制,后端分页查询机制,每次请求一部分数据,
  2. 这部分对于实时性,一致性的要求并不高,所以可以使用CDN,或者redis进行缓存

购买模块:对于一致性要求高,典型的原子性要求

解决方案:

  1. 后端使用事务控制,买家账户的减操作,卖家账户的增操作,以及商品数量的更新。

扩展
高并发的场景中,如果一本书是一个热点的商品,那么在这行数据上会产生大量的竞争。(秒杀)
Java高并发优化:中间件redis
热点数据的处理性能影响
主要是在事务的执行时间,一个事务必须等待另一个事务commit之后才能开始。
如果在客户端去控制事务的逻辑的话,其会受网络IO的影响
所以解决方法是

  1. 将事务的逻辑放在服务端。
  2. 使用存储过程优化:事务行级锁的持有时间
    这部分其实也不是redis所解决的问题

3. 消息队列,和redis的使用场景

为什么使用消息队列?

消息队列的特点:异步,削峰,解耦

  • 削峰:当流量大的时候,服务器承受能力不够,大量的请求流量全部涌入到服务器中,会对服务器造成问题。所以先将请求放在队列中,然后服务器去从队列中取请求去处理即可。
  • 异步:消息队列的特点,将不需要同步的逻辑放在消息队列中,可以减少用户的等待时间
  • 解耦:

为什么使用redis?

  1. 性能:对于一些耗时久,结果不频繁变动的sql,适合将运行结果放入缓存,提高查询速度
  2. 并发:高并发的情况下,所有请求直接访问数据库,数据库会出现连接异常。这个时候使用redis做一个缓冲操作,让请求先访问redis,而不是直接访问数据库。

3. Redis为什么这么快(特点)

  1. Redis是基于内存的NoSql(Redis 也支持持久化)
  2. Redis 采用单线程模式处理请求。这样做的原因有 2个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
  3. Redis还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
  4. Redis提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务

4. 使用Redis要注意的问题

1. 缓存与数据库的一致性问题

对于读多写少的高并发场景,我们会经常使用redis做缓存来进行优化。
但是当出现数据更新的时候,如何保证redis缓存与数据库的数据一致性问题。
核心:更新数据库和更新缓存这两个操作,是无法保证原子性的

一致性的解决方案
先更新数据库再让缓存失效,当数据库更新成功,缓存失效失败的时候,发送失败消息到一个消息队列中去,自己消费,再次让缓存失效,直到成功

2. 缓存雪崩

缓存雪崩是指设置缓存时采用了相同的过期时间,导致缓存在某一个时刻同时失效,或者缓存服务器宕机宕机导致缓存全面失效,请求全部转发到了DB层面,DB由于瞬间压力增大而导致崩溃。缓存失效导致的雪崩效应对底层系统的冲击是很大的。

解决方案:

  1. 事前:将缓存失效的时间分散,降低每一个缓存过期时间的重复率
  2. 事前:如果是因为缓存服务器故障导致的问题,一方面尽量保证整个 redis 集群的⾼可⽤性,发现机器宕机尽快补上。另一方面,应用程序中可以采用多级缓存
  3. 事中:限流,避免大量请求进入数据库
  4. 事后:利⽤ redis 持久化机制保存的数据尽快恢复缓存

3. 缓存穿透

缓存穿透说简单点就是⼤量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有
经过缓存这⼀层。举个例⼦:某个⿊客故意制造我们缓存中不存在的 key 发起⼤量请求,导致⼤量请求落到数据库。

解决方案:

  1. 缓存⽆效 key : 如果缓存和数据库都查不到某个 key 的数据就写⼀个到 redis 中去并设置过期时间(这种⽅式可以解决请求的 key 变化不频繁的情况,如果⿊客恶意攻击,每次构建不同的请求key,会导致 redis 中缓存⼤量⽆效的 key)
  2. 布隆过滤器:将不符合规则的key进行过滤

5. Redis 常⻅数据结构

String
Hash
list
set
zset

6. Redis 设置过期时间底层

  1. 定期删除:redis默认是每隔 100ms 就随机抽取⼀些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这⾥是随机抽取的。为什么要随机呢?你想⼀想假如 redis 存了⼏⼗万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很⼤的负载

  2. 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存⾥,除⾮你的系统去查⼀下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的

问题:

  • 但是仅仅通过设置过期时间还是有问题的。我们想⼀下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没⾛惰性删除,此时会怎么样?
    ⼤量过期key堆积在内存⾥,导致redis内存块耗尽

  • 怎么解决这个问题呢?
    redis 内存淘汰机制。

7. Redis 内存淘汰机制

  1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使⽤的数据淘汰
  2. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使⽤的数据
    淘汰
  3. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  4. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  5. allkeys-lru:当内存不⾜以容纳新写⼊数据时,在键空间中,移除最近最少使⽤的key(这个是最常⽤的)
  6. allkeys-lfu:当内存不⾜以容纳新写⼊数据时,在键空间中,移除最不经常使⽤的key
  7. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  8. no-eviction:禁⽌驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错。

8. Redis 持久化

  1. 快照(snapshotting,RDB)

Redis可以通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本(Redis主从结构,主要⽤来提⾼Redis性能),还可以将快照留在原地以便重启服务器的时候使⽤

在Redis的配置⽂件中存在三种不同的RDB 持久化⽅式,它们分别是:

  1. save 900 1 #在900秒(15分钟)之后,如果⾄少有1个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。
  2. save 300 10 #在300秒(5分钟)之后,如果⾄少有10个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。
  3. save 60 10000 #在60秒(1分钟)之后,如果⾄少有10000个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。
  1. 只追加⽂件(append-only file,AOF)

与快照持久化相⽐,AOF持久化 的实时性更好,因此已成为主流的持久化⽅案。默认情况下Redis没有开启AOF(append only file)⽅式的持久化,可以通过appendonly参数开启
开启AOF持久化后每执⾏⼀条会更改Redis中的数据的命令,Redis就会将该命令写⼊硬盘中的AOF⽂件。
AOF⽂件的保存位置和RDB⽂件的位置相同,都是通过dir参数设置的,默认的⽂件名是 appendonly.aof

在Redis的配置⽂件中存在三种不同的 AOF 持久化⽅式,它们分别是:

  1. appendfsync always #每次有数据修改发⽣时都会写⼊AOF⽂件,这样会严重降低Redis的速度
  2. appendfsync everysec #每秒钟同步⼀次,显示地将多个写命令同步到硬盘
  3. appendfsync no #让操作系统决定何时进⾏同步

9. Redis 事务

Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了⼀种将多个命令请求打包,然后⼀次性、按顺序地执⾏多个命令的机制,并且在事务执⾏期间,服务器不会中断事务⽽改去执⾏其他客户端的命令请求,它会将事务中的所有命令都执⾏完毕,然后才去处理其他客户端的命令请求。
在传统的关系式数据库中,常常⽤ ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原⼦性(Atomicity)、⼀致性(Consistency)和隔离性(Isolation),并且当 Redis 运⾏在某种特定的持久化模式下时,事务也具有持久性(Durability)

Redis同⼀个事务中如果有⼀条命令执⾏失败,其后的命令仍然会被执⾏,没有回滚。(来⾃ issue:关于Redis事务不是原⼦性问题 )

10. Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对⼀个 key 进⾏操作,但是最后执⾏的顺序和我们期望的顺序不同,这样也就导致了结果的不同

解决方案:
分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使⽤分布式锁,这样会影响性能)

11. Redis实现分布锁

java 中的Synchronized, Lock是单机环境中多线程的并发锁方案
那么在分布式的环境中,如何实现一个分布式锁呢?

SET lock_key random_value NX PX 5000
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。
解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来

12. 为什么不使用HashMap,而是要使用redis

  1. Redis 可以用几十G内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了
  2. Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了
  3. Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里
  4. Redis 缓存有过期机制,Map 本身无此功能
  • 作者:物语1995
  • 原文链接:https://blog.csdn.net/qq_37442823/article/details/120390813
    更新时间:2023年1月29日10:28:42 ,共 4574 字。