Redis篇:
一、Redis缓存击穿
定义:
缓存击穿,也称为热点Key问题,是指某个被高并发访问的热点数据在缓存中突然失效(如过期),此时大量请求直接访问数据库,导致数据库压力骤增,甚至可能崩溃。
解决方案:
- 设置永不过期:对于某些热点数据,可以设置为永不过期,然后通过后台异步更新缓存,保证这些数据始终存在于缓存中。
- 使用互斥锁:在缓存失效时,通过分布式锁确保只有一个请求能够查询数据库并更新缓存,其他请求则等待缓存更新完毕后直接从缓存中获取数据。这种方法可以避免多线程并发导致数据库压力过大。
- 逻辑过期:将过期时间设置在Redis的value中,通过逻辑去处理过期问题。当读取到数据时,如果发现数据已过期,则重新构建缓存数据。
import redis.clients.jedis.Jedis; public class CacheBreakthroughHandler { private static final String LOCK_KEY = "lock_key_for_data_x"; private static final String DATA_KEY = "data_x"; private static final long LOCK_EXPIRE_TIME = 10000; // 锁过期时间,单位毫秒 public String getDataX(Jedis jedis) { String data = jedis.get(DATA_KEY); if (data != null) { // 缓存命中,直接返回数据 return data; } // 缓存未命中,尝试加锁 String lockResult = jedis.set(LOCK_KEY, "locked", "NX", "PX", LOCK_EXPIRE_TIME); if ("OK".equals(lockResult)) { // 加锁成功,从数据库加载数据 data = loadDataFromDatabase(); // 将数据设置回缓存 jedis.setex(DATA_KEY, expireTime, data); // 释放锁(这里简单使用删除锁的方式,实际生产环境中可能需要更复杂的逻辑来确保锁的安全性) jedis.del(LOCK_KEY); } else { // 加锁失败,可能其他线程正在加载数据,稍后再试或直接从数据库读取(不推荐直接从数据库读取,因为这可能导致数据库压力过大) // 这里简单处理为稍后再试 try { Thread.sleep(100); // 等待一小段时间 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getDataX(jedis); // 递归调用,直到获取到数据或达到某种条件(如重试次数限制) } return data; } private String loadDataFromDatabase() { // 模拟从数据库加载数据 return "loaded_data_from_db"; } }
二、Redis缓存穿透
定义:
缓存穿透是指客户端请求的数据既不在缓存中,也不在数据库中。由于缓存未命中,系统会直接访问数据库,且由于查询结果为空,不会将该结果存入缓存,导致每次相同的请求都会打到数据库,造成数据库的压力增加,甚至可能被恶意利用进行“缓存穿透攻击”。
解决方案:
- 缓存空对象:当数据库查询结果为空时,将空结果也缓存起来,并设置较短的过期时间。这样,下次相同的请求就可以直接从缓存中返回空值,避免对数据库的访问。
- 使用布隆过滤器:布隆过滤器是一种概率型数据结构,可以用来快速判断某个元素是否存在于集合中。在缓存层之前使用布隆过滤器可以过滤掉那些一定不存在的数据请求,减少对数据库的访问。
- 限制请求频率:通过限制请求的频率,尤其是对某些可疑的IP或请求进行限流,避免恶意用户频繁发起缓存穿透攻击。
import redis.clients.jedis.Jedis; public class CachePenetrationHandler { private static final String DATA_KEY = "data_that_does_not_exist"; public String getDataThatDoesNotExist(Jedis jedis) { String data = jedis.get(DATA_KEY); if (data != null) { // 缓存命中,直接返回数据(这里应该是空字符串或null,表示数据不存在) return data; } // 缓存未命中,查询数据库(这里假设数据库也没有该数据) // 省略数据库查询逻辑,直接模拟结果为null // 将空结果缓存起来(这里简单使用set,实际中可能需要设置较短的过期时间) jedis.setex(DATA_KEY, 60, ""); // 假设设置60秒过期 // 返回null或特定标记表示数据不存在 return null; } }
三、Redis缓存雪崩
定义:
缓存雪崩是指由于大量缓存数据在同一时间过期,导致大量请求直接访问数据库,造成数据库压力骤增,甚至崩溃。
解决方案:
- 设置不同的过期时间:避免大量缓存设置相同的过期时间,可以通过给缓存的过期时间加上一个随机值来避免缓存同时失效。
- 增加缓存容量:如果缓存容量不足,可以考虑增加缓存节点数量或增大单个节点的容量,以降低缓存失效的概率。
- 使用Redis高可用架构:如Redis主从架构结合Sentinel或Redis Cluster,确保在缓存节点故障时能够自动切换到其他节点,保证缓存服务的持续运行。
- 设置本地缓存:在应用层面设置本地缓存,如Guava Cache、Ehcache等,当Redis缓存失效时,可以先从本地缓存中获取数据,避免直接访问数据库。
- 限流和降级策略:在缓存失效时,使用限流策略来限制对数据库的访问频率,避免数据库压力过大。同时,可以使用降级策略来提供备用的数据或服务,保证在缓存雪崩时系统仍然能够部分可用。
-
import redis.clients.jedis.Jedis; public class CacheAvalancheHandler { private static final String DATA_PREFIX = "data_"; public void setRandomExpireData(Jedis jedis, int index) { String dataKey = DATA_PREFIX + index; // 假设我们有一批数据需要缓存,并设置不同的过期时间 long expireTime = 3600 + (long) (Math.random() * 60 * 60); // 1小时到2小时之间的随机过期时间 jedis.setex(dataKey, (int) expireTime, "some_data_" + index); } // 假设这个方法被频繁调用以加载和缓存数据 // ... }
在这个缓存雪崩的解决方案示例中,我们并没有直接处理雪崩本身(因为雪崩通常是一个结果,而不是一个可以直接处理的问题),而是通过随机化过期时间来降低所有缓存同时失效的可能性,从而预防雪崩的发生。