缓存:数据交换的缓冲区,存储的数据的临时地方,读写性能较高。
步骤:
- 先从redis里面查询
- 缓存命中:直接返回结果
- 缓存未命中
- 从数据库里面查询
- 没有数据:返回null
- 有数据:存到redis里面,并返回
缓存更新策略:
1、内存淘汰:redis内存不足时,自动淘汰一部分数据;
2、超时剔除:设置TTL过期时间;
3、主动更新:查询数据库时就更新redis。
按业务场景去使用:
低一致性:内存淘汰
高一致性:主动更新,超时剔除作为兜底
主动更新策略
- Cache Aside Pattern调用者自己编码,更新数据库时更新缓存
- Read/Write Through Pattern缓存和数据库整合为一个服务,调用者使用该服务
- Write Behind Caching Pattern调用者仅仅操作缓存,由其他的线程异步将缓存数据持久化到数据库
缓存穿透
缓存穿透的定义
缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样每次请求都会穿透缓存直接访问数据库。如果大量这样的请求同时出现,可能会导致数据库压力过大,甚至造成数据库服务崩溃。
例如,攻击者故意使用一些不存在的用户 ID 频繁请求用户信息接口,由于这些用户 ID 对应的信息在缓存和数据库中都没有,就会使这些请求直接打到数据库上。
缓存穿透产生的原因
恶意攻击:攻击者通过构造大量不存在的键来频繁请求服务,试图使数据库过载。
业务逻辑问题:在业务代码中,可能存在没有对数据是否存在进行有效验证就直接查询数据库的情况。比如,在一个电商系统中,商品 ID 如果没有经过合法性检查,可能会有一些无效的 ID 被当作正常请求发送到数据库。
缓存穿透的解决方案
缓存空对象
原理:当从数据库查询数据发现不存在时,在缓存中缓存一个空对象(可以使用一个特定的标记值来表示空对象),并设置一个较短的过期时间。这样,下次同样的请求过来时,会在缓存中命中这个空对象,避免直接访问数据库。
优缺点:
优点:简单有效,能够快速解决大部分缓存穿透问题。
缺点:
- 如果恶意攻击者使用大量不同的不存在的键进行攻击,缓存中会存储大量的空对象,占用缓存空空间,可以将过期时间设置的短一点。
- 会造成数据短期的不一致。id不存在,缓存了一个空值,当新的数据来时,并且和当前缓存空对象的id一致。会造成redis缓存是空,但是实际上有这条数据。当该id数据被访问时,走redis取出的就会是空。可以给过期时间设置短一点,或者在插入数据时跟新一下缓存。
布隆过滤器(Bloom Filter)
原理:布隆过滤器是一种概率型数据结构,它可以用来判断一个元素是否在一个集合中。它的原理是通过多个哈希函数对元素进行哈希运算,将结果对应的位在一个位数组中置为 1。当查询一个元素是否存在时,对该元素进行同样的哈希运算,如果所有对应的位都是 1,则该元素可能存在;如果有任何一位是 0,则该元素一定不存在。
示例:使用 Google Guava 库中的布隆过滤器(这只是一个简单示例,实际应用中可能需要根据数据量和误判率等因素调整参数)。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;
public class BloomFilterExample {public static void main(String[] args) {// 创建一个布隆过滤器,预计插入1000个元素,误判率为0.01BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF - 8")), 1000, 0.01);// 假设这些是数据库中存在的用户IDString[] existingUserIds = {"1", "2", "3"};for (String userId : existingUserIds) {bloomFilter.put(userId);}// 检查用户ID是否可能存在String nonExistentUserId = "4";if (!bloomFilter.mightExist(nonExistentUserId)) {System.out.println("这个用户ID大概率不存在,无需查询数据库");}}
}
优缺点:
优点:可以高效地过滤掉大量不存在的元素,节省缓存空间和数据库查询次数。
缺点:实现复杂,存在一定的误判率,即有可能将实际上不存在的元素判断为可能存在,但可以通过调整参数来降低误判率。而且布隆过滤器本身也需要占用一定的内存空间。
主动避免缓存穿透
上面的都是被动去接受缓存穿透,但是可以主动去避免。
- 增强id的复杂性,避免被猜出id;
- 对传来的id做基本的格式校验,将不符合要求的id给pass掉;
- 做好热点参数的限流(微服务)
- 加强用户权限的校验,用户没有权限不能访问一些接口。
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略(微服务)
- 给业务添加多级缓存(微服务)
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
缓存重建业务较复杂 :查询数据库建立缓存耗时较长(几十、几百毫秒),在这个时间内可能有多个其他的线程也来访问,去查数据库,照成数据库压力过大。
解决方案:
互斥锁
多个线程来访问,当一个线程访问未命中时,获取锁,然后查数据库,建立缓存,释放锁;在这个时间内,其他的线程再来时,获取不到锁,一直等待带线程释放锁。
优点:保证一致性;
缺点:
其他的许多线程会等待该线程执行完成,性能下降;
可能引发死锁。
逻辑过期
适用于缓存热点key,提前有缓存预热。
把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。
假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程2去进行 以前的重构数据的逻辑,直到新开的线程2完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
优点:其他的线程无需等待;
缺点:不能保证数据的一致性;
缓存预热 :活动开始时,对于一些热点key,提前加到redis中,设置逻辑过期时间,所以说缓存一般是一定会命中的,未命中说明不是热点key,返回null;活动结束,再删除。