Redis学习路线(4)—— Redis实现项目缓存

一、什么是缓存

(一)概念:缓存就是数据交换的缓冲区(称为Cache),是存储数据的临时区域,一般读写性能较高。

(二)常见缓存: 浏览器缓存,服务器缓存,数据库缓存,CPU缓存,磁盘缓存。

(三)缓存的作用:

  • 降低后端负载
  • 提高读写效率,降低响应时间

(四)缓存的成本:

  • 数据一致性成本
  • 代码维护成本
  • 运维成本

二、缓存更新策略

(一)三种更新策略的对比

内存淘汰 超时剔除 主动更新
说明 Redis提供的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存 给缓存数据添加TTL,到期后自动删除缓存,下次查询时更新缓存 编写业务逻辑,在修改数据库的同时更新缓存
一致性 一般
维护成本

(二)业务场景

  • 低一致性需求: 使用内存淘汰机制,主要是针对几乎不改变的数据。
  • 高一致性需求: 主动更新,并以超时剔除作为协助方案,主要是针对频繁查询的缓存。

(三)主动更新策略

  • (1)Cache Aside Pattern: 由缓存调用者,同时更新数据库和缓存。(可控性更高)
    • 删除缓存还是更新缓存?
      • 更新缓存: 每次更新数据库都更新缓存,无效写操作较多。
      • 删除缓存: 更新数据库时让缓存失效,查询时再更新缓存。
    • (2)如何保证缓存与数据库的操作同时成功或失败?
      • 单体系统: 将缓存与数据库操作放在一个事务。
      • 分布式系统: 利用TCC等分布式事务方案。
    • (3)先操作缓存还是先操作数据库?
      • 先删除缓存,在操作数据库。
      • 先操作数据库,在删除缓存。(保证一致性的概率更高)
  • Read/Write Through Pattern: 缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。
  • Write Behind Caching Pattern: 调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。

三、缓存穿透、缓存雪崩、缓存击穿

(一)缓存穿透

1、缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

2、缓存穿透过程: 客户端发送不存在的id 》 Redis按照 id 查询(未查询到) 》 数据库根据 id 查询(未查询到) 》 返回客户端 》 客户端发送不存在的id。(这个过程若采用多线程循环攻击数据库,那么就会造成数据库带宽爆满,请求阻塞,系统资源耗尽等等问题)

3、缓存穿透常用解决方案:

  • (1)缓存空对象: 在用户第一次查询缓存和数据库时,都没有对应的值,则在缓存中存储一个对应的空值,并设置一个短期的TTL,当恶意用户短期内发起请求时,都会在缓存中查找到对应的值,避免了缓存穿透。
    • 优点: 实现简单,维护方便
    • 缺点: 额外的内存消耗、可能造成短期的不一致
  • (2)布隆过滤: 通过布隆过滤器,在客户端和Redis之间做一道防火墙,当用户访问到布隆过滤器时,布隆过滤器会查询自身是否存在请求值,若不存在则拒绝,若存在则放行查询。
    • 优点: 内存占用较少,没有多余key
    • 缺点: 实现复杂,存在误判可能
  • (3)增强id复杂度,避免被猜测id规律
  • (4)做好数据的基础格式校验
  • (5)加强用户权限校验
  • (6)做好热点参数的限流

4、布隆过滤器: 一种算法,通过将数据库的数据进行某一种hash值计算,并将这些hash值转化为二进制位存储到布隆过滤器中,客户端发送的请求中的数据,通过hash计算转化为二进制位在布隆过滤器中查找,若能找到则说明存在该数据。

(二)缓存雪崩

1、缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库。

2、缓存雪崩的过程: 客户端请求数据 》 Redis查询(Redis部分缓存缺失或Redis宕机) 》 查询数据库(大量请求到达,把数据库也给宕机了)

3、解决方案:

  • (1)给不同的key的TTL添加随机值(避免同时失效)
  • (2)利用Redis集群提高服务的可用性(Redis哨兵机制监听选举主Redis,主从Redis保证数据安全性)
  • (3)给缓存业务添加降级限流策略
  • (4)给业务添加多级缓存

(三)缓存击穿

1、缓存击穿是指高并发缓存重建业务复杂的key突然失效,无数的请求访问在瞬间给数据库带来巨大的冲击。

2、常见的解决方案:

  • (1)互斥锁: 在一个线程进入缓存重建阶段时,获取互斥锁,其它线程无法进行获取互斥锁,并自旋尝试获取锁,直到互斥锁释放。
    • 缺点: 线程锁住了缓存,时间过长会造成业务阻塞。
  • (2)逻辑过期: 在存储一个数据时,添加一个逻辑上的过期时间,到期后直接移除即可。

3、两种解决方案的区别

解决方案 优点 缺点
互斥锁
  • 没有额外的内存消耗
  • 保证一致性
  • 实现简单
  • 线程需要等待,性能受影响
  • 可能发生死锁
逻辑过期
  • 现成无需等待,性能较好
  • 不保证一致性
  • 有额外内存消耗
  • 实现复杂

四、缓存工具封装

基于StringRedisTemplate封装一个缓存工具类。

  • 方法1: 将任意Java对象序列化为json并存储在string类型的key中,并设置TTL过期时间。
  • 方法2: 将任意Java对象序列化为json并存储在string类型的key中,并可以设置逻辑过期时间,用于处理缓存击穿问题。
  • 方法3: 根据指定key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题。
  • 方法4: 根据指定key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题。

(一)工具类

@Slf4j
@Component
public class CacheClient {
    
    
	
	private final StringRedisTemplate redisTemplate;

	public CacheClient(StringRedisTemplate redisTempalte) {
    
    
		this.redisTemplate = redisTempalte;
	}

	public String toJsonStr(Object value) {
    
    
		return JSONObject.toJSONString(value)
	}

	public <T> String toJsonObj(String json, Class<T> type) {
    
    
		return JSONObject.parseObject(json, type)
	}
	
}

(二)实现缓存工具方法

方法1:序列化并设置TTL

	public void set(String key, Object data, Long time, TimeUnit unit) {
    
    
		redisTemplate.opsForValue().set(key, this.toJsonStr(data), time, unit);
	}

方法2:序列化并设置逻辑过期

	public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
    
    
		//1、设置逻辑过期,RedisData -> (R Data, LocalDateTime dateTime)
		RedisData redisData = new RedisData(value, LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
		
		//2、RedisData 写入Redis
		redisTemplate.opsForValue().set(key, this.toJsonStr(redisData));
	}

方法3:缓存空值解决缓存穿透

	public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    
    
		//1、查看Cache是否命中
		String key = keyPrefix + id;
		String json = stringRedisTemplate.opsForValue().get(key);
		
		if(!json.isEmpty()){
    
    
			//命中,判断为有值,直接返回
			return this.toJsonObj(json, type);
		}
		
		if(Objects.isNull(json)){
    
    
			//命中,判断为空白,返回错误信息
			return null;
		}
		
		//未命中,查看数据库
		R r = dbFallback.apply(id);
		//若不存在对应数据,则打入Redis一个空值,并返回错误信息
		if (r == null) {
    
    
			redisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
			return null;
		}
		
		//若存在,则返回并设置TTL
		this.set(key, r, time, unit);
		
		//返回该值
		return r;
	}

方法4: 逻辑过期解决缓存击穿

public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
    
    

	//1、查看Cache是否命中
	String key = keyPrefix + id;
	String json = stringRedisTemplate.opsForValue().get(key);

	if(Objects.isEmpty(json)){
    
    
		//命中,判断为空白,返回错误信息
		return null;
	}
		
	RedisData redisData = this.toJsonObject(json, Redis.Class);
	R r = type.cast(redisData.getData());
	LocalDateTime time = redisData.getDateTime();
		
	//若逻辑时间小于现实时间则返回当前对象
	if(time.isBefore(LocalDateTime.now())){
    
    
		redisData = JSON.parseObject(json, RedisData.class);
		if(redisData.getDateTime < LocalDateTime.now()){
    
    
			return type.cast(redisData.getData());
		}
	}
		
	//过期了,获取互斥锁
	String lockKey = "lock:shop:"+id;
	boolean isLock = tryLock(lockKey);
	//尝试获取互斥锁,如果事变,则睡眠重试
	if(!isLock){
    
    
		//失败则休眠后重试
		Thread.sleep(50);
		return queryWithLogicalExpire(keyPrefix, id, type, dbFallback, time, unit);
	}
	// 重建数据
	Executors.newFixedThreadPool(10).submit(() -> {
    
    
		try{
    
    
			//	查询数据库
			R r1 = dbFallback.apply(id);
			this.setWithLogicalExpire(id, r1, time, unit);
		}catch(Exception e){
    
    
			throw new RuntimeException(e);
		}finally{
    
    
			unlock(lockKey);
		}
	});
	return r;
}

private boolean tryLock(String key) {
    
    
	Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "", 10, TimeUnit.SECONDS);
	return Boolean.getBoolean(flag);
}

private void unlock(String key) {
    
    
	redisTemplate.delete(key);
}

猜你喜欢

转载自blog.csdn.net/Zain_horse/article/details/132002608