最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

最近發(fā)現(xiàn)線上監(jiān)控有個SQL調(diào)用量很大,但是方法的調(diào)用量不是很大,查看接口實現(xiàn),發(fā)現(xiàn)接口是做了緩存操作的,使用Spring cache緩存注解結(jié)合tair實現(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)用量巨大,給了黑客可乘之機,風(fēng)險還是很大的。

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

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

基于這個思路我把Cache的實現(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類默認是一個空對象,這里給了個內(nèi)部類:

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

取緩存的get方法實現(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)鍵問題所在(具體實現(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是一個接口,它的實現(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)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • SpringBoot全局異常處理方式

    SpringBoot全局異常處理方式

    本文詳細介紹了Spring?Boot中異常處理的多種方案,包括基于請求轉(zhuǎn)發(fā)、異常處理器、過濾器等方式,并對其特點、實現(xiàn)方式和適用場景進行了詳細介紹,作者推薦了基于請求轉(zhuǎn)發(fā)和異常處理器+請求轉(zhuǎn)發(fā)補充的方案,認為它們能實現(xiàn)統(tǒng)一的全局異常處理,并能自定義統(tǒng)一響應(yīng)信息格式
    2024-12-12
  • Java 并發(fā)編程學(xué)習(xí)筆記之核心理論基礎(chǔ)

    Java 并發(fā)編程學(xué)習(xí)筆記之核心理論基礎(chǔ)

    編寫優(yōu)質(zhì)的并發(fā)代碼是一件難度極高的事情。Java語言從第一版本開始內(nèi)置了對多線程的支持,這一點在當(dāng)年是非常了不起的,但是當(dāng)我們對并發(fā)編程有了更深刻的認識和更多的實踐后,實現(xiàn)并發(fā)編程就有了更多的方案和更好的選擇。本文是對并發(fā)編程的核心理論做了下小結(jié)
    2016-05-05
  • Springboot中的@ConditionalOnBean注解詳細解讀

    Springboot中的@ConditionalOnBean注解詳細解讀

    這篇文章主要介紹了Springboot中的@ConditionalOnBean注解詳細解讀,@ConditionalOnMissingBean注解兩個類,一個Computer類,一個配置類,想要完成;如果容器中沒有Computer類,就注入備用電腦Computer類,如果有Computer就不注入,需要的朋友可以參考下
    2023-11-11
  • SpringBoot整合Shiro的方法詳解

    SpringBoot整合Shiro的方法詳解

    Apache?Shiro是一個java安全(權(quán)限)框架,Shiro可以非常容易的開發(fā)出足夠好的應(yīng)用,其不僅可以用在javase環(huán)境,也可以用在javaee環(huán)境。本文介紹了SpringBoot整合Shiro的方法,需要的可以參考一下
    2022-05-05
  • SpringCloud Gateway路由核心原理解析

    SpringCloud Gateway路由核心原理解析

    本文主要介紹了SpringCloudGateway的基礎(chǔ)構(gòu)建塊、工作原理以及核心原理解析,SpringCloudGateway是Spring官方基于SpringSpringBoot和ProjectReactor等技術(shù)開發(fā)的網(wǎng)關(guān),旨在為微服務(wù)架構(gòu)提供一種簡單而有效的統(tǒng)一的API路由管理方式
    2024-10-10
  • Java Iterator接口實現(xiàn)代碼解析

    Java Iterator接口實現(xiàn)代碼解析

    這篇文章主要介紹了Java Iterator接口實現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-05-05
  • Java進階教程之運行時類型識別RTTI機制

    Java進階教程之運行時類型識別RTTI機制

    這篇文章主要介紹了Java進階教程之運行時類型識別RTTI機制,在Java運行時,RTTI維護類的相關(guān)信息,比如多態(tài)(polymorphism)就是基于RTTI實現(xiàn)的,需要的朋友可以參考下
    2014-09-09
  • Mybatis Log控制臺如何輸出打印SQL語句

    Mybatis Log控制臺如何輸出打印SQL語句

    這篇文章主要介紹了Mybatis Log控制臺如何輸出打印SQL語句,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • MyBatis-Plus標簽@TableField之fill自動填充方式

    MyBatis-Plus標簽@TableField之fill自動填充方式

    這篇文章主要介紹了MyBatis-Plus標簽@TableField之fill自動填充方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-06-06
  • SpringBoot邏輯異常統(tǒng)一處理方法

    SpringBoot邏輯異常統(tǒng)一處理方法

    這篇文章主要介紹了SpringBoot邏輯異常統(tǒng)一處理方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-10-10

最新評論

蚌埠市| 隆化县| 新丰县| 神池县| 南涧| 钟山县| 皋兰县| 巴中市| 三明市| 天镇县| 瑞昌市| 南溪县| 房产| 广东省| 应城市| 临城县| 宝坻区| 古田县| 揭阳市| 南平市| 天等县| 资源县| 乐山市| 夏津县| 汶上县| 交口县| 桃园县| 兴义市| 南溪县| 泗洪县| 兴隆县| 库车县| 宣城市| 芒康县| 高淳县| 星座| 隆安县| 孟津县| 周至县| 琼结县| 内黄县|