欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

spring?cache注解@Cacheable緩存穿透詳解

 更新時間:2021年12月28日 10:02:03   作者:chengbinbbs  
這篇文章主要介紹了spring?cache注解@Cacheable緩存穿透詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教

最近發(fā)現(xiàn)線上監(jiān)控有個SQL調(diào)用量很大,但是方法的調(diào)用量不是很大,查看接口實(shí)現(xiàn),發(fā)現(xiàn)接口是做了緩存操作的,使用Spring cache緩存注解結(jié)合tair實(shí)現(xiàn)緩存操作。

但是為啥SQL調(diào)用量這么大,難道緩存沒有生效。測試發(fā)現(xiàn)緩存是正常的,分析了代碼發(fā)現(xiàn),代碼存在緩存穿透的風(fēng)險。

具體注解是這樣的

@Cacheable(value = "storeDeliveryCoverage", key = "#sellerId + '|' + #cityCode", unless = "#result == null")

unless = "#result == null"表明接口返回值不為空的時候才緩存,如果線上有大量不合法的請求參數(shù)過來,由于為空的不會緩存起來,每次請求都打到DB上,導(dǎo)致DB的sql調(diào)用量巨大,給了黑客可乘之機(jī),風(fēng)險還是很大的。

找到原因之后就修改,查詢結(jié)果為空的時候兜底一個null,把這句unless = "#result == null"條件去掉測試了一下,發(fā)現(xiàn)為空的話還是不會緩存。于是debug分析了一波源碼,終于發(fā)現(xiàn)原來是tair的問題。

由于tair自身的特性,無法緩存null。既然無法緩存null,那我們就兜底一個空對象進(jìn)去,取出來的時候把空對象轉(zhuǎn)化為null。

基于這個思路我把Cache的實(shí)現(xiàn)改造了一下

@Override
    public void put(Object key, Object value) {
        if (value == null) {
            // 為空的話,兜底一個空對象,防止緩存穿透(由于tair自身特性不允許緩存null對象的原因,這里緩存一個空對象)
            value = new Nil();
        }
        if (value instanceof Serializable) {
            final String tairKey = String.format("%s:%s", this.name, key);
            final ResultCode resultCode = this.tairManager.put(
                    this.namespace,
                    tairKey,
                    (Serializable) value,
                    0,
                    this.timeout
            );
            if (resultCode != ResultCode.SUCCESS) {
                TairSpringCache.log.error(
                        String.format(
                                "[CachePut]: unable to put %s => %s into tair due to: %s",
                                key,
                                value,
                                resultCode.getMessage()
                        )
                );
            }
        } else {
            throw new RuntimeException(
                    String.format(
                            "[CachePut]: value %s is not Serializable",
                            value
                    )
            );
        }
    }

Nil類默認(rèn)是一個空對象,這里給了個內(nèi)部類:

static class Nil implements Serializable {
        private static final long serialVersionUID = -9138993336039047508L;
    }

取緩存的get方法實(shí)現(xiàn)

@Override
    public ValueWrapper get(Object key) {
        final String tairKey = String.format("%s:%s", this.name, key);
        final Result<DataEntry> result = this.tairManager.get(this.namespace, tairKey);
        if (result.isSuccess() && (result.getRc() == ResultCode.SUCCESS)) {
            final Object obj = result.getValue().getValue();
            // 緩存為空兜底的是Nil對象,這里返回的時候需要轉(zhuǎn)為null
            if (obj instanceof Nil) {
                return null;
            }
            return () -> obj;
        }
        return null;
    }

改好了之后,測試一下,結(jié)果發(fā)現(xiàn)還是沒有生效,緩存沒有兜底,請求都打到DB上了。

debug走一遍,看了下Cache的源碼,終于發(fā)現(xiàn)關(guān)鍵問題所在(具體實(shí)現(xiàn)流程參考上一篇:Spring Cache- 緩存攔截器( CacheInterceptor))

private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// Special handling of synchronized invocation
		if (contexts.isSynchronized()) {
			CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
			if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
				Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
				Cache cache = context.getCaches().iterator().next();
				try {
					return wrapCacheValue(method, cache.get(key, new Callable<Object>() {
						@Override
						public Object call() throws Exception {
							return unwrapReturnValue(invokeOperation(invoker));
						}
					}));
				}
				catch (Cache.ValueRetrievalException ex) {
					// The invoker wraps any Throwable in a ThrowableWrapper instance so we
					// can just make sure that one bubbles up the stack.
					throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
				}
			}
			else {
				// No caching required, only call the underlying method
				return invokeOperation(invoker);
			}
		}
		// 處理beforeIntercepte=true的緩存刪除操作
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);
		// 從緩存中查找,是否有匹配@Cacheable的緩存數(shù)據(jù)
		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
		// 如果@Cacheable沒有被緩存,那么就需要將數(shù)據(jù)緩存起來,這里將@Cacheable操作收集成CachePutRequest集合,以便后續(xù)做@CachePut緩存數(shù)據(jù)存放。
		List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}
		Object cacheValue;
		Object returnValue;
		//如果沒有@CachePut操作,就使用@Cacheable獲取的結(jié)果(可能也沒有@Cableable,所以result可能為空)。
		if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
			//如果沒有@CachePut操作,并且cacheHit不為空,說明命中緩存了,直接返回緩存結(jié)果
			cacheValue = cacheHit.get();
			returnValue = wrapCacheValue(method, cacheValue);
		}
		else {
			// 否則執(zhí)行具體方法內(nèi)容,返回緩存的結(jié)果
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}
		// Collect any explicit @CachePuts
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
		// Process any collected put requests, either from @CachePut or a @Cacheable miss
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}
		// Process any late evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
		return returnValue;
	}

根據(jù)key從緩存中查找,返回的結(jié)果是ValueWrapper,它是返回結(jié)果的包裝器:

private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
		Object result = CacheOperationExpressionEvaluator.NO_RESULT;
		for (CacheOperationContext context : contexts) {
			if (isConditionPassing(context, result)) {
				Object key = generateKey(context, result);
				Cache.ValueWrapper cached = findInCaches(context, key);
				if (cached != null) {
					return cached;
				}
				else {
					if (logger.isTraceEnabled()) {
						logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
					}
				}
			}
		}
		return null;
	}
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
		for (Cache cache : context.getCaches()) {
			Cache.ValueWrapper wrapper = doGet(cache, key);
			if (wrapper != null) {
				if (logger.isTraceEnabled()) {
					logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
				}
				return wrapper;
			}
		}
		return null;
	}

這里判斷緩存是否命中的邏輯是根據(jù)cacheHit是否為空,而cacheHit是ValueWrapper類型,查看ValueWrapper是一個接口,它的實(shí)現(xiàn)類是SimpleValueWrapper,這是一個包裝器,將緩存的結(jié)果包裝起來了。

而我們前面的get方法取緩存的時候如果為Nil對象,返回的是null,這樣緩存判斷出來是沒有命中,即cacheHit==null,就會去執(zhí)行具體方法朔源。

所以到這里已經(jīng)很清晰了,關(guān)鍵問題是get取緩存的結(jié)果如果是兜底的Nil對象,應(yīng)該返回new SimpleValueWrapper(null)。

應(yīng)該返回包裝器,包裝的是緩存的對象為null。

測試了一下,發(fā)現(xiàn)ok了

具體源碼如下:

/**
 * 基于tair的緩存,適配spring緩存框架
 */
public class TairSpringCache implements Cache {
    private static final Logger log = LoggerFactory.getLogger(TairSpringCache.class);
    private TairManager tairManager;
    private final String name;
    private int namespace;
    private int timeout;
    public TairSpringCache(String name, TairManager tairManager, int namespace) {
        this(name, tairManager, namespace, 0);
    }
    public TairSpringCache(String name, TairManager tairManager, int namespace, int timeout) {
        this.name = name;
        this.tairManager = tairManager;
        this.namespace = namespace;
        this.timeout = timeout;
    }
    @Override
    public String getName() {
        return this.name;
    }
    @Override
    public Object getNativeCache() {
        return this.tairManager;
    }
    @Override
    public ValueWrapper get(Object key) {
        final String tairKey = String.format("%s:%s", this.name, key);
        final Result<DataEntry> result = this.tairManager.get(this.namespace, tairKey);
        if (result.isSuccess() && (result.getRc() == ResultCode.SUCCESS)) {
            final Object obj = result.getValue().getValue();
            // 緩存為空兜底的是Nil對象,這里返回的時候需要轉(zhuǎn)為null
            if (obj instanceof Nil) {
                return () -> null;
            }
            return () -> obj;
        }
        return null;
    }
    @Override
    public <T> T get(Object key, Class<T> type) {
        return (T) this.get(key).get();
    }
    public <T> T get(Object o, Callable<T> callable) {
        return null;
    }
    @Override
    public void put(Object key, Object value) {
        if (value == null) {
            // 為空的話,兜底一個空對象,防止緩存穿透(由于tair自身特性不允許緩存null對象的原因,這里緩存一個空對象)
            value = new Nil();
        }
        if (value instanceof Serializable) {
            final String tairKey = String.format("%s:%s", this.name, key);
            final ResultCode resultCode = this.tairManager.put(
                    this.namespace,
                    tairKey,
                    (Serializable) value,
                    0,
                    this.timeout
            );
            if (resultCode != ResultCode.SUCCESS) {
                TairSpringCache.log.error(
                        String.format(
                                "[CachePut]: unable to put %s => %s into tair due to: %s",
                                key,
                                value,
                                resultCode.getMessage()
                        )
                );
            }
        } else {
            throw new RuntimeException(
                    String.format(
                            "[CachePut]: value %s is not Serializable",
                            value
                    )
            );
        }
    }
    public ValueWrapper putIfAbsent(Object key, Object value) {
        final ValueWrapper vw = this.get(key);
        if (vw.get() == null) {
            this.put(key, value);
        }
        return vw;
    }
    @Override
    public void evict(Object key) {
        final String tairKey = String.format("%s:%s", this.name, key);
        final ResultCode resultCode = this.tairManager.delete(this.namespace, tairKey);
        if ((resultCode == ResultCode.SUCCESS)
                || (resultCode == ResultCode.DATANOTEXSITS)
                || (resultCode == ResultCode.DATAEXPIRED)) {
            return;
        }
        else {
            final String errMsg = String.format(
                    "[CacheDelete]: unable to evict key %s, resultCode: %s",
                    key,
                    resultCode
            );
            TairSpringCache.log.error(errMsg);
            throw new RuntimeException(errMsg);
        }
    }
    @Override
    public void clear() {
        //TODO fgz: implement here later
    }
    public void setTairManager(TairManager tairManager) {
        this.tairManager = tairManager;
    }
    public void setNamespace(int namespace) {
        this.namespace = namespace;
    }
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
    static class Nil implements Serializable {
        private static final long serialVersionUID = -9138993336039047508L;
    }
}

測試用例就不貼了。

以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 聊聊Java 成員變量賦值和構(gòu)造方法誰先執(zhí)行的問題

    聊聊Java 成員變量賦值和構(gòu)造方法誰先執(zhí)行的問題

    這篇文章主要介紹了聊聊Java 成員變量賦值和構(gòu)造方法誰先執(zhí)行的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-10-10
  • Kotlin傳遞可變長參數(shù)給Java可變參數(shù)實(shí)例代碼

    Kotlin傳遞可變長參數(shù)給Java可變參數(shù)實(shí)例代碼

    這篇文章主要介紹了Kotlin傳遞可變長參數(shù)給Java可變參數(shù)實(shí)例代碼,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下
    2018-01-01
  • 關(guān)于文件合并與修改md5值的問題

    關(guān)于文件合并與修改md5值的問題

    這篇文章主要介紹了關(guān)于文件合并與修改md5值的問題,使用本博客的方法,不僅僅可以修改md5值,還可以達(dá)到隱藏文件的目的,需要的朋友可以參考下
    2023-04-04
  • 如何獲取springboot打成jar后的classpath

    如何獲取springboot打成jar后的classpath

    這篇文章主要介紹了如何獲取springboot打成jar后的classpath問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • SpringBoot實(shí)現(xiàn)熱部署詳解

    SpringBoot實(shí)現(xiàn)熱部署詳解

    SpringBoot熱部署是一種開發(fā)時極為有用的功能,它能夠讓開發(fā)人員在代碼修改后無需手動重啟應(yīng)用程序就能立即看到變化的效果,所以我本文就給打擊介紹一下為什么要使用熱部署以及實(shí)現(xiàn)熱部署的方式,需要的朋友可以參考下
    2023-07-07
  • SpringBoot基于自定義注解實(shí)現(xiàn)切面編程

    SpringBoot基于自定義注解實(shí)現(xiàn)切面編程

    這篇文章主要介紹了SpringBoot基于自定義注解實(shí)現(xiàn)切面編程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-11-11
  • SpringBoot中使用AOP打印接口日志的方法

    SpringBoot中使用AOP打印接口日志的方法

    本篇文章主要介紹了SpringBoot中使用AOP打印接口日志的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-05-05
  • MyBatisPlus 查詢selectOne方法實(shí)現(xiàn)

    MyBatisPlus 查詢selectOne方法實(shí)現(xiàn)

    本文主要介紹了MyBatisPlus 查詢selectOne方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • SpringMVC結(jié)合ajaxfileupload.js實(shí)現(xiàn)文件無刷新上傳

    SpringMVC結(jié)合ajaxfileupload.js實(shí)現(xiàn)文件無刷新上傳

    這篇文章主要介紹了SpringMVC結(jié)合ajaxfileupload.js實(shí)現(xiàn)文件無刷新上傳,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-10-10
  • java http token請求代碼實(shí)例

    java http token請求代碼實(shí)例

    這篇文章主要介紹了java http token請求,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-03-03

最新評論