高并发环境下缓存的重要性
在高并发环境下,例如淘宝双11秒杀活动,几分钟内上亿用户涌入平台,短时间内产生的海量请求如果直接涌向数据库,将会对数据库产生巨大的压力。由于磁盘I/O的速度远低于内存访问速度,如果不加以控制,数据库将不堪重负,进而导致服务中断。为了避免这种情况,通常会在数据库之前加入一层缓存机制。缓存不仅可以减轻数据库的压力,还可以显著提高系统响应速度,因为缓存数据可以直接从内存中读取,响应时间极短。
分布式缓存的必要性
随着业务规模的增长,单台机器的内存资源和处理能力变得有限。如果大量使用本地缓存,会导致相同的数据在不同节点间重复存储,造成内存资源浪费。因此,分布式缓存应运而生,它允许数据分布在多个节点上,既能够充分利用集群中的内存资源,又可以提供更高的数据访问速度和容错能力。
缓存应用场景
-
页面缓存
- 缓存Web页面的部分或全部内容,包括但不限于HTML、CSS和图片等静态资源,以减少数据库的读取次数。
-
应用对象缓存
- 作为ORM框架的二级缓存,用于存储频繁访问的数据对象,减少对数据库的查询,提高应用程序性能。
- 解决分布式Web部署中的session同步问题,缓存session会话状态及横向扩展时的状态数据。
-
public class SessionManager { private RedisTemplate<String, Object> redisTemplate; public void setSession(String sessionId, Object data) { redisTemplate.opsForValue().set("session:" + sessionId, data); } public Object getSession(String sessionId) { return redisTemplate.opsForValue().get("session:" + sessionId); } }
-
并行处理
- 在大数据处理或任务调度场景中,缓存可用于存储中间计算结果,便于各节点之间的共享和重用。
- 假设有一个任务调度器,它会将中间结果存储在缓存中:
-
def process_data(chunk): # Process the data chunk... intermediate_result = ... cache.set('intermediate_' + str(chunk.id), intermediate_result)
-
云计算领域
- 提供分布式缓存服务,支持云应用快速访问热点数据,提高用户体验。
缓存常见的问题及解决方案
缓存穿透
- 定义: 当用户请求的数据在缓存和数据库中均不存在时,称为缓存穿透。
- 影响: 大量的无效请求会直接打到数据库,可能导致数据库负载过高甚至崩溃。
- 解决方案:
-
缓存空值: 对查询不到的数据也设置一个缓存条目,设置较短的有效期。
-
布隆过滤器: 使用布隆过滤器预先判断数据是否存在,减少无效查询。
布隆过滤器是一个位数组,用于检测元素是否在一个集合中。当元素加入集合时,通过几个不同的哈希函数将该元素映射到位数组的不同位置并将它们置为1。查询时,如果这些位置都是1,则认为元素可能存在;如果其中任何一个位置是0,则元素肯定不存在。
-
缓存雪崩
- 定义: 当大量缓存条目在同一时间过期时,请求会集中地落在数据库上,造成瞬间压力过大。
- 影响: 数据库可能因短时间内承受过多请求而宕机。
- 解决方案:
- 随机化过期时间: 给定一个基础过期时间,在此基础上加上一个随机值,使得过期时间分布更均匀。
-
public void setWithExpire(String key, String value, int baseExpireSeconds) { int expire = baseExpireSeconds + new Random().nextInt(baseExpireSeconds / 3); redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS); }
缓存击穿
- 定义: 大量并发请求在缓存失效后同时访问数据库的现象。
- 影响: 可能导致数据库瞬间压力增大。
- 解决方案:
- 互斥锁: 利用分布式锁机制,确保同一时间内只有一个请求去加载数据。
-
public String get(String key) { String value = redis.get(key); if (value == null) { // 缓存值过期 String unique_key = systemId + ":" + key; if (redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) { // 设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(unique_key); } else { // 其他线程已经到数据库取值并回写到缓存了,可以重试获取缓存值 sleep(50); get(key); // 重试 } } else { return value; } }
- 过期时间延长: 设置较长的过期时间,并结合随机化策略,减少同时过期的概率。
缓存预热
- 定义: 在系统启动初期,主动将数据加载到缓存中。
- 目的: 避免系统上线初期大量请求直接访问数据库。
- 实施方式:
- 手工预热: 上线时手动触发预热逻辑。
- 自动预热: 在系统启动时自动执行预热脚本。
- 定时刷新: 定期检查并更新缓存数据。
缓存降级
- 定义: 在系统面临高负载或部分功能不可用时,降低服务的质量以保证核心功能正常运行。
- 策略:
- 根据系统健康状况自动或手动降级非核心服务。
- 为不同的异常级别设置预案,如日志记录、报警通知等。
保证缓存与数据库双写的一致性
-
方案一: 先删除缓存再更新数据库
- 优点: 更新数据后,下次请求必定能读取到最新数据。
- 缺点: 存在短暂的不一致窗口。
-
public void updateData(String key, String newValue) { redis.delete(key); db.update(key, newValue); }
-
方案二: 先更新数据库再删除缓存
- 优点: 减少了不一致窗口的时间。
- 缺点: 仍有可能出现短暂不一致。
-
public void updateData(String key, String newValue) { db.update(key, newValue); redis.delete(key); }
-
方案三: 异步更新缓存
- 实现: 使用消息队列来异步更新缓存。
- 优点: 保证了更新顺序,减少了不一致风险。
- 缺点: 增加了系统复杂度,需要处理消息队列的可靠性问题。
-
public void updateDataAsync(String key, String newValue) { db.update(key, newValue); rabbitTemplate.convertAndSend("cache-update-exchange", "cache-update-key", key); } @RabbitListener(queues = "cache-update-queue") public void handleCacheUpdate(String key) { String value = db.get(key); redis.set(key, value); }
总结
在高并发场景下,合理利用缓存技术能够极大地改善系统性能,但也带来了诸如缓存穿透、雪崩、击穿等问题。通过上述各种解决方案的应用,可以有效地缓解这些问题,保障系统的稳定性和可用性。此外,缓存预热和降级策略也是应对突发流量的重要手段,能够在关键时刻保护系统不受损害。最后,在设计缓存策略时,还需要考虑到数据的一致性问题,选择合适的双写策略以满足业务需求。
标签:缓存,String,数据库,value,key,public,分布式 From: https://blog.csdn.net/weixin_45049746/article/details/141754941