文章目录
- Redis与缓存
- 一致性问题
- 大Key问题
- 缓存穿透
- 缓存击穿
- 缓存雪崩
Redis与缓存
Redis
作为缓存具有高性能、丰富的数据结构和灵活的过期机制等优点。由于Redis
将数据存储在内存中,它能提供极低的延迟和高吞吐量,适合用于缓存数据库查询结果、会话数据和实时数据处理等场景。Redis
的多种数据结构支持不同的缓存需求,如缓存静态内容、实现简单的消息队列,以及处理实时统计信息。
用户第一次访问数据库中的某些数据。整个过程会比较慢,因为是从硬盘上读取的。如果将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快,但随之而来的也会存在一些问题。
一致性问题
只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题。缓存一致性问题发生在缓存中的数据与源数据之间存在不一致的情况。这种不一致可能会导致系统中的数据错误或不准确。
解决一致性问题的关键在于,当源数据发生更改时,缓存中的数据也需要更新。
在更新源数据时,同时更新缓存和源数据库。这种方式保持了数据的一致性,因为所有写操作都会同时在缓存和数据库中完成。写操作的延迟可能增加,特别是在高负载情况下,更新操作可能成为性能瓶颈。适用于对数据一致性要求高的场景,例如金融系统和实时数据处理系统,其中一致性比性能更重要。
另一种方法是异步更新,先更新缓存,再通过后台任务异步更新源数据库。这种方式提高了写操作的性能,因为数据库更新是异步进行的,减少了写入延迟。缓存的写入速度较快,有助于提升用户体验。但数据库和缓存之间可能出现最终一致性问题,数据库更新的延迟可能导致缓存数据与源数据库不一致。适用于数据一致性要求可以容忍一定延迟的场景,如在线购物网站和社交媒体平台,其中用户体验和性能优于即时一致性。
缓存失效策略在读取数据时,如果缓存中不存在数据或数据已过期,会从源数据库加载数据并更新缓存。这种方式通过重新加载来保持数据的一致性。缓存失效或数据过期时,总是从源数据库中获取最新数据,避免了缓存和数据库间的数据不一致问题。读取延迟可能增加,特别是缓存频繁失效时,且频繁的数据库访问可能增加负载。适用于读取操作较多的场景,例如内容分发网络或新闻网站,通过缓存失效平衡数据一致性和性能。
当缓存或数据库更新失败时,也会导致数据不一致的问题。为了解决这个问题,可以采取几种方法。可以设置自动重试机制,如果更新操作失败,系统会尝试重新执行更新,增加成功的可能性,虽然这样可能会增加系统的负担。另一种方法是使用备用缓存,在主缓存更新失败时,将数据写入备用缓存,并在主缓存恢复正常时进行同步,这样可以保持系统的正常运转。记录更新失败的情况,并触发报警,也能帮助快速发现问题,防止问题长时间存在。还可以将缓存更新操作放在后台任务中,并设置补偿机制来修复数据不一致。如果后台任务失败,补偿机制会尝试重新同步数据。
大Key问题
所谓的大Key
问题是指某个Key
的value
比较大,所以本质上是大value
问题。因为Key
往往是程序可以自行设置的,value
往往不受程序控制,因此可能导致value
很大。大Key
占用的内存非常多,可能导致Redis
实例的内存使用量急剧增加。大Key的读取、写入或删除可能会显著拖慢Redis
的性能。例如,操作一个非常大的列表会占用大量的CPU和IO资源,导致其他操作的响应时间变慢。
大Key
问题一般是由于业务方案设计不合理,没有预见value
的动态增长问题产生的。一直往value
塞数据,没有删除机制,迟早要爆炸或数据没有合理做分片,将大Key
变成小Key
。在线上一般通过设置Redis
监控,及时发现和处理大Key问题。可以使用工具监控键的大小,避免存储异常大的数据项。
解决思路:
- 可在应用层或客户端设置最大键值大小限制,防止大
Key
被写入Redis
。 - 定期检查
Redis
中的大Key
,并进行必要的清理或优化操作。 - 如果
Redis
中已经存在大Key
,根据大Key
的实际用途可以分为可删除和不可删除,如果发现某些大Key
并非热Key
就可以在DB中查询使用,则可以在Redis
中删掉。
如果不可删除,则需要拆分大Key
,将大Key
拆分成多个小Key
,然后进行删除。
缓存穿透
缓存穿透是指查询请求绕过缓存直接访问数据库,通常是因为请求中的数据在缓存中不存在。这个问题可能导致缓存失效,增加数据库负担,并影响系统性能。缓存穿透的原因通常是,用户请求的数据在缓存和数据库中都不存在,或者是请求的数据在缓存中未命中,直接查询数据库,并未将结果正确地缓存起来。
解决方法:
- 布隆过滤器:布隆过滤器是一种空间高效的数据结构,用于判断某个元素是否在集合中。它可以减少对数据库的访问次数,通过在缓存层使用布隆过滤器,来快速判断请求的数据是否可能存在于数据库中。将可能存在的键加入布隆过滤器。在每次查询之前,先检查布隆过滤器。如果布隆过滤器显示数据不存在,则直接返回空值或错误,不访问数据库。
public class BloomFilterExample { private BloomFilter<String> bloomFilter; private StringRedisTemplate redisTemplate; public BloomFilterExample(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; // Initialize Bloom Filter with an expected insertions and false positive probability this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 100000, 0.01); } public String getData(String key) { // Check if the key is in the Bloom Filter if (!bloomFilter.mightContain(key)) { return null; // Key definitely not in cache or DB } // Check cache String value = redisTemplate.opsForValue().get(key); if (value != null) { return value; } // Load from DB (simulate) value = loadFromDatabase(key); // Cache the result and add to Bloom Filter if (value != null) { redisTemplate.opsForValue().set(key, value); bloomFilter.put(key); } return value; } private String loadFromDatabase(String key) { // Simulate DB access return "DatabaseValueFor" + key; } }
- 缓存空对象:当数据库查询返回空结果时,将空结果缓存到
Redis
中,使用一个特殊的标识,如空字符串、null
、特定的空值对象等。后续相同的查询可以直接从缓存中获取空结果,避免再次访问数据库。设置空对象的缓存时间较短,避免长时间缓存无效数据。public class CacheEmptyObjectExample { private static final String EMPTY_OBJECT_PLACEHOLDER = "EMPTY"; private StringRedisTemplate redisTemplate; public CacheEmptyObjectExample(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public String getData(String key) { // Check cache String value = redisTemplate.opsForValue().get(key); if (EMPTY_OBJECT_PLACEHOLDER.equals(value)) { return null; // Data definitely does not exist } if (value != null) { return value; } // Load from DB (simulate) value = loadFromDatabase(key); if (value != null) { redisTemplate.opsForValue().set(key, value); } else { redisTemplate.opsForValue().set(key, EMPTY_OBJECT_PLACEHOLDER); } return value; } private String loadFromDatabase(String key) { // Simulate DB access return null; // Simulate no data found } }
缓存击穿
缓存击穿 是指在缓存中某个热点数据的缓存失效时,多个请求同时访问数据库,导致数据库压力剧增的情况。这种问题通常发生在缓存数据过期或被删除时,如果请求大量集中在短时间内,可能会导致数据库负载急剧上升。
解决方法:
- 加锁机制:在缓存失效时,对数据的访问进行加锁,保证只有一个请求能够从数据库中加载数据并更新缓存。其他请求需要等待锁释放后,才能获取缓存中的数据。
public class CacheLockExample { private StringRedisTemplate redisTemplate; private RedisTemplate<String, Object> redisLockTemplate; private static final String LOCK_KEY_PREFIX = "lock:"; public CacheLockExample(StringRedisTemplate redisTemplate, RedisTemplate<String, Object> redisLockTemplate) { this.redisTemplate = redisTemplate; this.redisLockTemplate = redisLockTemplate; } public String getData(String key) { String cacheKey = "cache:" + key; String lockKey = LOCK_KEY_PREFIX + key; // Check cache String value = redisTemplate.opsForValue().get(cacheKey); if (value != null) { return value; } // Acquire lock Boolean lockAcquired = redisLockTemplate.opsForValue().setIfAbsent(lockKey, "locked"); if (lockAcquired != null && lockAcquired) { try { // Load from DB (simulate) value = loadFromDatabase(key); // Cache the result if (value != null) { redisTemplate.opsForValue().set(cacheKey, value); } } finally { // Release lock redisLockTemplate.delete(lockKey); } } else { // Wait for lock to be released and retry try { Thread.sleep(100); // Wait for 100 milliseconds } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return redisTemplate.opsForValue().get(cacheKey); } return value; } private String loadFromDatabase(String key) { // Simulate DB access return "DatabaseValueFor" + key; } }
- 缓存预热:在缓存过期前,提前将热点数据加载到缓存中,减少缓存失效对数据库的冲击。这可以通过定期刷新缓存或使用缓存预热策略来实现。
public class CachePreheatExample { private StringRedisTemplate redisTemplate; public CachePreheatExample(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public void preheatCache() { // Simulate loading all hotspot data for (String key : getHotspotKeys()) { String value = loadFromDatabase(key); if (value != null) { redisTemplate.opsForValue().set("cache:" + key, value); } } } private Iterable<String> getHotspotKeys() { // Simulate getting all hotspot keys return List.of("key1", "key2", "key3"); } private String loadFromDatabase(String key) { // Simulate DB access return "DatabaseValueFor" + key; } }
缓存雪崩
缓存雪崩 是指当大量缓存同时失效或遭遇问题时,造成大量请求同时涌入数据库,导致数据库负载过重,从而引发服务不可用的情况。这种情况常见于缓存失效时间集中或缓存服务宕机等场景。
解决方法:
- 缓存过期时间随机化:通过对缓存的过期时间进行随机化,避免所有缓存同时过期。根据业务需求设置合理的缓存过期时间。避免缓存过期时间设置过长或过短,导致缓存失效的集中现象。
public class CacheRandomExpirationExample { private StringRedisTemplate redisTemplate; private static final int EXPIRATION_TIME = 600; // 10 minutes public CacheRandomExpirationExample(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public String getData(String key) { String cacheKey = "cache:" + key; String value = redisTemplate.opsForValue().get(cacheKey); if (value != null) { return value; } // Load from DB (simulate) value = loadFromDatabase(key); if (value != null) { // Set cache with random expiration time between 10 and 20 minutes int expiration = EXPIRATION_TIME + (int) (Math.random() * 600); redisTemplate.opsForValue().set(cacheKey, value, expiration, TimeUnit.SECONDS); } return value; } private String loadFromDatabase(String key) { // Simulate DB access return "DatabaseValueFor" + key; } }
- 使用多级缓存:引入多级缓存策略,比如本地缓存和分布式缓存结合使用。这样即使分布式缓存失效,本地缓存仍能提供服务,减少对数据库的压力。
public class MultiLevelCacheExample { private StringRedisTemplate redisTemplate; private Cache<String, String> localCache; public MultiLevelCacheExample(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; this.localCache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .build(); } public String getData(String key) { // Check local cache first String value = localCache.getIfPresent(key); if (value != null) { return value; } String cacheKey = "cache:" + key; value = redisTemplate.opsForValue().get(cacheKey); if (value != null) { localCache.put(key, value); return value; } // Load from DB (simulate) value = loadFromDatabase(key); if (value != null) { redisTemplate.opsForValue().set(cacheKey, value, 10, TimeUnit.MINUTES); localCache.put(key, value); } return value; } private String loadFromDatabase(String key) { // Simulate DB access return "DatabaseValueFor" + key; } }