缓存那些事

本文探讨了缓存在系统中的重要性及其解决的性能问题,同时深入讨论了缓存存在的数据一致性、雪崩和击穿问题,并提出了相应的解决方案,包括先更新数据库再删除缓存策略、使用连接池和限流器、布隆过滤器等。文章还提到了通过Spring AOP和SpEL表达式解析器优雅地处理缓存逻辑。
摘要由CSDN通过智能技术生成

概要

缓存是现在系统中必不可少的模块,并且已经成为了高并发高性能架构的一个关键组件。从硬件缓存、到软件缓存;从底层的操作系统到上层的应用系统,缓存无处不在,在我理解,要深入掌握这门技术,需要先掌握缓存的思想。

缓存解决的问题

说白了,缓存就是计算机系统中最常见的空间换时间的思想的体现,为的就是尽最大可能提升计算机软件系统的性能。举几个例子如:
1、内存中的数据需要放到CPU中去计算,不是当需要计算的时候再从内存中一个数据一个数据的去取,而是有高速cpu缓存一次性保存很多数据,用于提升内存和cpu之间的数据交换。
2、普通Web应用,通常我们从数据库获取数据,然后返回给浏览器进行展示,数据库的数据到浏览器,之间经历我们的数据库,后端web应用(服务器内存),网络,再到浏览器,用户想要更快的获取到数据,那么就可以利用缓存,提前把数据放到web应用、甚至放到浏览器。
3、复杂的系统 ,用户获取数据的路线可能是下面的样子:
浏览器 》 CDN(内容分发网络) 》 代理层 》 缓存中间件
》 应用层 》
》应用层缓存|缓存中间件 》 数据库缓存 》 数据库

缓存存在的问题

数据一致性问题

从上面描述的两个场景不难看出,缓存使用时,最明显存在的问题就是数据实时性问题,可能用户获取到的数据不是我们最新的数据,即缓存与数据库数据一致性问题。

解决方案

1、当然我们可以采用完全串行化的方式(即保证缓存操作与数据库操作的原子性)保证缓存与数据库的数据一致性问题。但是这与我们缓存通常要解决的高并发下问题相违背。
2、下面简单说下几种方式,其实都不能保证强一致性,其中前面3中方式不推荐,推荐第4种并且详细说明(需要了解详细为什么的可以查看文章https://blog.csdn.net/chang384915878/article/details/86756463
https://blog.csdn.net/qq_27384769/article/details/79499373
https://blog.kido.site/2018/11/24/db-and-cache-preface/)
a、先更新缓存,再更新数据库,考虑写与写之间的并发,会有问题
b、先更新数据库,再更新缓存,考虑写与写之间的并发,会有问题
c、先删除缓存,再更新数据库,考虑读写之间的并发,有问题
d、先更新数据库,再删除缓存,推荐,但也存在较小几率有问题,比如,读先来读数据,发现缓存没有,从数据库获取了数据,准备更新缓存,此时写更新了数据库,然后删除了缓存完成了写操作;此刻,读线程最后再用旧数据更新了缓存,则导致缓存里的数据是旧数据,与数据库里的新数据不一致。这种情况只会出现缓存里没有数据的情况下。通过设置过期时间或者下次再有数据更新时消除不一致。
3、阿里开源canal,mysql与redis之间的增量同步中间服务,详细使用方式可以查看
https://blog.csdn.net/lyl0724/article/details/80528428
https://blog.csdn.net/weixin_40606441/article/details/79840205

缓存雪崩

问题出现:
redis持久化淘汰
redis缓存过期失效
redis重启、升级
导致缓存查不到,短时间内如果来大量请求,可能对数据库造成压力。
1、采用数据库连接池可以避免对数据库造成连接压力。但是压力总量不变,只是数据库层面限流了。
2、将压力提前,所以需要在应用层、业务层限流,在查询数据库前添加限流器,进入方法,先拿缓存,拿不到就获取semphere,拿到锁的先查缓存,查不到再查数据库,查到数据库再更新缓存。容错、限流、降级

缓存击穿

问题出现:
当频繁访问数据库本身就不存在的数据时,不论访问多少次,都不会在缓存中找到,这就绕过了缓存层,造成了缓存击穿
问题如何解决:
1、查询到数据库中不存在就给redis插入空值,但是这个解决不了大量不存在ID的查询,因为会造成redis存储大量没用的控制信息。
2、filter,先判断是否存在,把所有存在的数据的key加载到内存或者redis。就可以先判断是否存在了。
3、方案2会造成空间大量浪费,所以继续优化,只用一个bit来表示某个key是否存在,引出布隆过滤器。
BloomFilter
布隆过滤器采用bit和hash的方式实现,空间占用小,但是会有少量因为hash取模算法导致相同的slot位置而冲突导致的存在误判(不存在的不会误判),意思是判断存在,其实可能不存在,和更新数据困难的问题。布隆过滤器需要不断维护。
这个误判很少,1、可以通过设置null值解决。2、通过多次hash减少误判

redis三方模块redis-bloom,可以通过在配置文件中配置loadModules引入该模块的功能。
RedisBloomFilter

结合缓存雪崩里的逻辑:
进入方法,先用bloomfilter判断是否存在,先拿缓存,拿不到就获取semphere,拿到锁的先查缓存,查不到再查数据库,查到数据库再更新缓存。

解决方案

如果要解决上面提到的缓存雪崩与缓存穿透问题,往往需要在用到缓存的业务代码中增加大量的逻辑,导致原先简单的业务代码变得复杂,甚至难以维护,但是我们可以使用spring AOP实现自定义缓存注解优雅的处理上诉过程
注意:
1、spring面向切面编程的方式
2、我们可以使用spring提供的spel表达式解析器
SpelExpressionParser
借用网易云老师的代码:
a、核心切面类

package com.study.cache.stampeding.annotations;

import java.lang.reflect.Method;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import com.study.cache.stampeding.bloom.RedisBloomFilter;

@Component
@Aspect
public class CoustomCacheAspect {
	private Logger logger = LoggerFactory.getLogger(this.getClass());
    
	@Resource(name = "mainRedisTemplate") 
	StringRedisTemplate mainRedisTemplate;
	
	@Autowired
	RedisBloomFilter filter;
	
    // 数据库限流,根据数据库连接数来定义大小
    Semaphore semaphore = new Semaphore(30);

    @Pointcut("@annotation(com.study.cache.stampeding.annotations.CoustomCache)")
    public void cachePointcut() {
    }

    // 定义相应的事件
    @Around("cachePointcut()")
    public Object doCache(ProceedingJoinPoint joinPoint) {
    	Object value = null;
    	
    	CoustomCache cacheAnnotation = findCoustomCache(joinPoint);
    	// 解析缓存Key
        String cacheKey = parseCacheKey(joinPoint);
        
        
        // 在缓存之前去进行过滤
        String bloomFilterName = cacheAnnotation.bloomFilterName();
    	boolean exists = filter.exists(bloomFilterName, cacheKey);
    	if(! exists) {
    		logger.warn(Thread.currentThread().getName()+" 您需要的商品是不存在的+++++++++++++++++++++++++++");
    		return "您需要的商品是不存在的";
    	}
        
        // 1、 判定缓存中是否存在
        value = mainRedisTemplate.opsForValue().get(cacheKey);
        if (value != null) {
        	logger.debug("从缓存中读取到值:" + value);
            return value;
        }
        
        // 访问数据库进行限流
        try {
        	
			if(semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
				
				value = mainRedisTemplate.opsForValue().get(cacheKey);
		        if (value != null) {
		        	logger.debug("从缓存中读取到值:" + value);
		            return value;
		        }
				
				// 交给服务层方法实现,从数据库获取
	            value = joinPoint.proceed();
				
				// 塞到缓存,过期时间10S
				final String v = value.toString();
				mainRedisTemplate.execute((RedisCallback<Boolean>) conn -> {
					return conn.setEx(cacheKey.getBytes(), 120, v.getBytes());
				});
			}else { // semaphore.tryAcquire(5, TimeUnit.SECONDS) 超时怎么办?
				// 再去获取一遍缓存,说不定已经有请求构建好了缓存。
				value = mainRedisTemplate.opsForValue().get(cacheKey);
				if(value != null) {
					logger.debug("等待后,再次从缓存获得");
					return value;
				}
    			
				// 缓存尚未构建好,进行服务降级,容错
    			// 友好的提示,对不起,票已售空、11.11 提示稍后付款;客官您慢些;
    			// 不断降低我们的预期目标, 外星人、小黑、华为、小米
				logger.debug("服务降级——容错处理");
			}
		
    	} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		} catch (Throwable e) {
			logger.error(e.getMessage(), e);
		}finally {
    		try {
				semaphore.acquire();
			} catch (InterruptedException e) {
				Thread.currentThread().interrupt();
			}
    	}
        return value;
    }
    
    private CoustomCache findCoustomCache(ProceedingJoinPoint joinPoint) {
		CoustomCache cacheAnnotation;
		try {
			MethodSignature signature = (MethodSignature) joinPoint.getSignature();
			Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
			cacheAnnotation = method.getAnnotation(CoustomCache.class);
			return cacheAnnotation;
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		}
		return null;
    }
    
    
    /**
     * 获取缓存Key
     * @param joinPoint
     * @return
     */
    private String parseCacheKey(ProceedingJoinPoint joinPoint) {
    	CoustomCache cacheAnnotation;
		// 解析
    	String cacheKey = null;
		try {
			// 0-1、 当前方法上注解的内容
			MethodSignature signature = (MethodSignature) joinPoint.getSignature();
			Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
			cacheAnnotation = findCoustomCache(joinPoint);
			String keyEl = cacheAnnotation.key();
			
			// 0-2、 前提条件:拿到作为key的依据  - 解析springEL表达式
			// 创建解析器
			ExpressionParser parser = new SpelExpressionParser();
			Expression expression = parser.parseExpression(keyEl);
			EvaluationContext context = new StandardEvaluationContext(); // 参数
			// 添加参数
			Object[] args = joinPoint.getArgs();
			DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
			String[] parameterNames = discover.getParameterNames(method);
			for (int i = 0; i < parameterNames.length; i++) {
			    context.setVariable(parameterNames[i], args[i].toString());
			}
			String key = expression.getValue(context).toString();
			cacheKey = cacheAnnotation.prefix() == null ? "" : cacheAnnotation.prefix() + key;
		} catch (ParseException e) {
			e.printStackTrace();
		} catch (EvaluationException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		}
        
        return cacheKey;
    }


}

b、注解类

package com.study.cache.stampeding.annotations;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义的缓存注解
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CoustomCache {
    /**
     * key的规则,可以使用springEL表达式,可以使用方法执行的一些参数
     */
    String key();
    
    /**
     * 缓存key的前缀
     * @return
     */
    String prefix();
    
    /**
     * 采用布隆过滤器的名称
     * @return
     */
    String bloomFilterName();
}

c、使用

    @CoustomCache(key = "#goodsId", prefix = "goodsStock-", bloomFilterName = "goodsBloomFilter")
    public Object queryStockByAnn(final String goodsId) {
    	// CRUD,只需要关系业务代码,交给码农去做
    	return databaseService.queryFromDatabase(goodsId);
    }

总结

最近工作比较忙,把以前的笔记整理了下形成了此篇文章,很多地方没有详细深入与画图举例,现在这打个标记,后续希望自己能够沉下来做一个完成的中间件的总结。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值