" />

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

Redis+Caffeine兩級緩存的實現

 更新時間:2022年06月22日 09:19:02   作者:1_2_3_4_5_上山打老虎  
本文主要介紹了Redis+Caffeine兩級緩存的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

在高性能的服務架構設計中,緩存是一個不可或缺的環(huán)節(jié)。在實際的項目中,我們通常會將一些熱點數據存儲到Redis或MemCache這類緩存中間件中,只有當緩存的訪問沒有命中時再查詢數據庫。在提升訪問速度的同時,也能降低數據庫的壓力。

隨著不斷的發(fā)展,這一架構也產生了改進,在一些場景下可能單純使用Redis類的遠程緩存已經不夠了,還需要進一步配合本地緩存使用,例如Guava cache或Caffeine,從而再次提升程序的響應速度與服務性能。于是,就產生了使用本地緩存作為一級緩存,再加上遠程緩存作為二級緩存的兩級緩存架構。

在先不考慮并發(fā)等復雜問題的情況下,兩級緩存的訪問流程可以用下面這張圖來表示:

優(yōu)點與問題

那么,使用兩級緩存相比單純使用遠程緩存,具有什么優(yōu)勢呢?

  • 本地緩存基于本地環(huán)境的內存,訪問速度非??欤瑢τ谝恍┳兏l率低、實時性要求低的數據,可以放在本地緩存中,提升訪問速度;
  • 使用本地緩存能夠減少和Redis類的遠程緩存間的數據交互,減少網絡I/O開銷,降低這一過程中在網絡通信上的耗時 ;

但是在設計中,還是要考慮一些問題的,例如數據一致性問題。首先,兩級緩存與數據庫的數據要保持一致,一旦數據發(fā)生了修改,在修改數據庫的同時,本地緩存、遠程緩存應該同步更新。

另外,如果是分布式環(huán)境下,一級緩存之間也會存在一致性問題,當一個節(jié)點下的本地緩存修改后,需要通知其他節(jié)點也刷新本地緩存中的數據,否則會出現讀取到過期數據的情況,這一問題可以通過類似于Redis中的發(fā)布/訂閱功能解決。

此外,緩存的過期時間、過期策略以及多線程訪問的問題也都需要考慮進去,不過我們今天暫時先不考慮這些問題,先看一下如何簡單高效的在代碼中實現兩級緩存的管理。

準備工作

在簡單梳理了一下要面對的問題后,下面開始兩級緩存的代碼實戰(zhàn),我們整合號稱最強本地緩存的Caffeine作為一級緩存、性能之王的Redis作為二級緩存。首先建一個springboot項目,引入緩存要用到的相關的依賴:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.8.1</version>
</dependency>

在application.yml中配置Redis的連接信息:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 10000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

在下面的例子中,我們將使用RedisTemplate來對redis進行讀寫操作,RedisTemplate使用前需要配置一下ConnectionFactory和序列化方式,這一過程比較簡單就不貼出代碼了。

下面我們在單機環(huán)境下,將按照對業(yè)務侵入性的不同程度,分三個版本來實現兩級緩存的使用。

V1.0版本

我們可以通過手動操作Caffeine中的Cache對象來緩存數據,它是一個類似Map的數據結構,以key作為索引,value存儲數據。在使用Cache前,需要先配置一下相關參數:

@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<String,Object> caffeineCache(){
        return Caffeine.newBuilder()
                .initialCapacity(128)//初始大小
                .maximumSize(1024)//最大數量
                .expireAfterWrite(60, TimeUnit.SECONDS)//過期時間
                .build();
    }
}

簡單解釋一下Cache相關的幾個參數的意義:

  • initialCapacity:初始緩存空大?。?/li>
  • maximumSize:緩存的最大數量,設置這個值可以避免出現內存溢出;
  • expireAfterWrite:指定緩存的過期時間,是最后一次寫操作后的一個時間,這里;

此外,緩存的過期策略也可以通過expireAfterAccess或refreshAfterWrite指定。

在創(chuàng)建完成Cache后,我們就可以在業(yè)務代碼中注入并使用它了。在沒有使用任何緩存前,一個只有簡單的Service層代碼是下面這樣的,只有crud操作:

@Service
@AllArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final OrderMapper orderMapper;
 
    @Override
    public Order getOrderById(Long id) {  
        Order order = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
              .eq(Order::getId, id));    
        return order;
    }
    
    @Override
    public void updateOrder(Order order) {      
        orderMapper.updateById(order);
    }
    
    @Override
    public void deleteOrder(Long id) {
        orderMapper.deleteById(id);
    }
}

接下來,對上面的OrderService進行改造,在執(zhí)行正常業(yè)務外再加上操作兩級緩存的代碼,先看改造后的查詢操作:

public Order getOrderById(Long id) {
    String key = CacheConstant.ORDER + id;
    Order order = (Order) cache.get(key,
            k -> {
                //先查詢 Redis
                Object obj = redisTemplate.opsForValue().get(k);
                if (Objects.nonNull(obj)) {
                    log.info("get data from redis");
                    return obj;
                }
 
                // Redis沒有則查詢 DB
                log.info("get data from database");
                Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
                        .eq(Order::getId, id));
                redisTemplate.opsForValue().set(k, myOrder, 120, TimeUnit.SECONDS);
                return myOrder;
            });
    return order;
}

在Cache的get方法中,會先從緩存中進行查找,如果找到緩存的值那么直接返回。如果沒有找到則執(zhí)行后面的方法,并把結果加入到緩存中。

因此上面的邏輯就是先查找Caffeine中的緩存,沒有的話查找Redis,Redis再不命中則查詢數據庫,寫入Redis緩存的操作需要手動寫入,而Caffeine的寫入由get方法自己完成。

在上面的例子中,設置Caffeine的過期時間為60秒,而Redis的過期時間為120秒,下面進行測試,首先看第一次接口調用時,進行了數據庫的查詢:

而在之后60秒內訪問接口時,都沒有打印打任何sql或自定義的日志內容,說明接口沒有查詢Redis或數據庫,直接從Caffeine中讀取了緩存。

等到距離第一次調用接口進行緩存的60秒后,再次調用接口:

可以看到這時從Redis中讀取了數據,因為這時Caffeine中的緩存已經過期了,但是Redis中的緩存沒有過期仍然可用。

下面再來看一下修改操作,代碼在原先的基礎上添加了手動修改Redis和Caffeine緩存的邏輯:

public void updateOrder(Order order) {
    log.info("update order data");
    String key=CacheConstant.ORDER + order.getId();
    orderMapper.updateById(order);
    //修改 Redis
    redisTemplate.opsForValue().set(key,order,120, TimeUnit.SECONDS);
    // 修改本地緩存
    cache.put(key,order);
}

看一下下面圖中接口的調用、以及緩存的刷新過程。可以看到在更新數據后,同步刷新了緩存中的內容,在之后的訪問接口時不查詢數據庫,也可以拿到正確的結果:

最后再來看一下刪除操作,在刪除數據的同時,手動移除Reids和Caffeine中的緩存:

public void deleteOrder(Long id) {
    log.info("delete order");
    orderMapper.deleteById(id);
    String key= CacheConstant.ORDER + id;
    redisTemplate.delete(key);
    cache.invalidate(key);
}

我們在刪除某個緩存后,再次調用之前的查詢接口時,又會出現重新查詢數據庫的情況:

簡單地演示到此為止,可以看到上面這種使用緩存的方式,雖然看起來沒什么大問題,但是對代碼的入侵性比較強。在業(yè)務處理的過程中要由我們頻繁地操作兩級緩存,會給開發(fā)人員帶來很大的負擔。那么,有什么方法能夠簡化這一過程呢?我們看看下一個版本。

V2.0版本

在spring項目中,提供了CacheManager接口和一些注解,允許讓我們通過注解的方式來操作緩存。先來看一下常用的幾個注解說明:

  • @Cacheable:根據鍵從緩存中取值,如果緩存存在,那么獲取緩存成功之后,直接返回這個緩存的結果。如果緩存不存在,那么執(zhí)行方法,并將結果放入緩存中。
  • @CachePut:不管之前的鍵對應的緩存是否存在,都執(zhí)行方法,并將結果強制放入緩存。
  • @CacheEvict:執(zhí)行完方法后,會移除掉緩存中的數據。

如果要使用上面這幾個注解管理緩存的話,我們就不需要配置V1版本中的那個類型為Cache的Bean了,而是需要配置spring中的CacheManager的相關參數,具體參數的配置和之前一樣:

@Configuration
public class CacheManagerConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager=new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(128)
                .maximumSize(1024)
                .expireAfterWrite(60, TimeUnit.SECONDS));
        return cacheManager;
    }
}

然后在啟動類上再添加上@EnableCaching注解,就可以在項目中基于注解來使用Caffeine的緩存支持了。下面,再次對Service層代碼進行改造。

首先,還是改造查詢方法,在方法上添加@Cacheable注解:

@Cacheable(value = "order",key = "#id")
//@Cacheable(cacheNames = "order",key = "#p0")
public Order getOrderById(Long id) {
    String key= CacheConstant.ORDER + id;
    //先查詢 Redis
    Object obj = redisTemplate.opsForValue().get(key);
    if (Objects.nonNull(obj)){
        log.info("get data from redis");
        return (Order) obj;
    }
    // Redis沒有則查詢 DB
    log.info("get data from database");
    Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
            .eq(Order::getId, id));
    redisTemplate.opsForValue().set(key,myOrder,120, TimeUnit.SECONDS);
    return myOrder;
}

@Cacheable注解的屬性多達9個,好在我們日常使用時只需要配置兩個常用的就可以了。其中value和cacheNames互為別名關系,表示當前方法的結果會被緩存在哪個Cache上,應用中通過cacheName來對Cache進行隔離,每個cacheName對應一個Cache實現。value和cacheNames可以是一個數組,綁定多個Cache。

而另一個重要屬性key,用來指定緩存方法的返回結果時對應的key,這個屬性支持使用SpringEL表達式。通常情況下,我們可以使用下面幾種方式作為key:

#參數名
#參數對象.屬性名
#p參數對應下標

在上面的代碼中,我們看到添加了@Cacheable注解后,在代碼中只需要保留原有的業(yè)務處理邏輯和操作Redis部分的代碼即可,Caffeine部分的緩存就交給spring處理了。

下面,我們再來改造一下更新方法,同樣,使用@CachePut注解后移除掉手動更新Cache的操作:

@CachePut(cacheNames = "order",key = "#order.id")
public Order updateOrder(Order order) {
    log.info("update order data");
    orderMapper.updateById(order);
    //修改 Redis
    redisTemplate.opsForValue().set(CacheConstant.ORDER + order.getId(),
            order, 120, TimeUnit.SECONDS);
    return order;
}

注意,這里和V1版本的代碼有一點區(qū)別,在之前的更新操作方法中,是沒有返回值的void類型,但是這里需要修改返回值的類型,否則會緩存一個空對象到緩存中對應的key上。當下次執(zhí)行查詢操作時,會直接返回空對象給調用方,而不會執(zhí)行方法中查詢數據庫或Redis的操作。

最后,刪除方法的改造就很簡單了,使用@CacheEvict注解,方法中只需要刪除Redis中的緩存即可:

@CacheEvict(cacheNames = "order",key = "#id")
public void deleteOrder(Long id) {
    log.info("delete order");
    orderMapper.deleteById(id);
    redisTemplate.delete(CacheConstant.ORDER + id);
}

可以看到,借助spring中的CacheManager和Cache相關的注解,對V1版本的代碼經過改進后,可以把全手動操作兩級緩存的強入侵代碼方式,改進為本地緩存交給spring管理,Redis緩存手動修改的半入侵方式。那么,還能進一步改造,使之成為對業(yè)務代碼完全無入侵的方式嗎?

V3.0版本

模仿spring通過注解管理緩存的方式,我們也可以選擇自定義注解,然后在切面中處理緩存,從而將對業(yè)務代碼的入侵降到最低。

首先定義一個注解,用于添加在需要操作緩存的方法上:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {
    String cacheName();
    String key(); //支持springEl表達式
    long l2TimeOut() default 120;
    CacheType type() default CacheType.FULL;
}

我們使用cacheName + key作為緩存的真正key(僅存在一個Cache中,不做CacheName隔離),l2TimeOut為可以設置的二級緩存Redis的過期時間,type是一個枚舉類型的變量,表示操作緩存的類型,枚舉類型定義如下:

public enum CacheType {
    FULL,   //存取
    PUT,    //只存
    DELETE  //刪除
}

因為要使key支持springEl表達式,所以需要寫一個方法,使用表達式解析器解析參數:

public static String parse(String elString, TreeMap<String,Object> map){
    elString=String.format("#{%s}",elString);
    //創(chuàng)建表達式解析器
    ExpressionParser parser = new SpelExpressionParser();
    //通過evaluationContext.setVariable可以在上下文中設定變量。
    EvaluationContext context = new StandardEvaluationContext();
    map.entrySet().forEach(entry->
        context.setVariable(entry.getKey(),entry.getValue())
    );
 
    //解析表達式
    Expression expression = parser.parseExpression(elString, new TemplateParserContext());
    //使用Expression.getValue()獲取表達式的值,這里傳入了Evaluation上下文
    String value = expression.getValue(context, String.class);
    return value;
}

參數中的elString對應的就是注解中key的值,map是將原方法的參數封裝后的結果。簡單進行一下測試:

public void test() {
    String elString="#order.money";
    String elString2="#user";
    String elString3="#p0";   
 
    TreeMap<String,Object> map=new TreeMap<>();
    Order order = new Order();
    order.setId(111L);
    order.setMoney(123D);
    map.put("order",order);
    map.put("user","Hydra");
 
    String val = parse(elString, map);
    String val2 = parse(elString2, map);
    String val3 = parse(elString3, map);
 
    System.out.println(val);
    System.out.println(val2);
    System.out.println(val3);
}

執(zhí)行結果如下,可以看到支持按照參數名稱、參數對象的屬性名稱讀取,但是不支持按照參數下標讀取,暫時留個小坑以后再處理。

123.0
Hydra
null

至于Cache相關參數的配置,我們沿用V1版本中的配置即可。準備工作做完了,下面我們定義切面,在切面中操作Cache來讀寫Caffeine的緩存,操作RedisTemplate讀寫Redis緩存。

@Slf4j @Component @Aspect 
@AllArgsConstructor
public class CacheAspect {
    private final Cache cache;
    private final RedisTemplate redisTemplate;
 
    @Pointcut("@annotation(com.cn.dc.annotation.DoubleCache)")
    public void cacheAspect() {
    }
 
    @Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
 
        //拼接解析springEl表達式的map
        String[] paramNames = signature.getParameterNames();
        Object[] args = point.getArgs();
        TreeMap<String, Object> treeMap = new TreeMap<>();
        for (int i = 0; i < paramNames.length; i++) {
            treeMap.put(paramNames[i],args[i]);
        }
 
        DoubleCache annotation = method.getAnnotation(DoubleCache.class);
        String elResult = ElParser.parse(annotation.key(), treeMap);
        String realKey = annotation.cacheName() + CacheConstant.COLON + elResult;
 
        //強制更新
        if (annotation.type()== CacheType.PUT){
            Object object = point.proceed();
            redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
            cache.put(realKey, object);
            return object;
        }
        //刪除
        else if (annotation.type()== CacheType.DELETE){
            redisTemplate.delete(realKey);
            cache.invalidate(realKey);
            return point.proceed();
        }
 
        //讀寫,查詢Caffeine
        Object caffeineCache = cache.getIfPresent(realKey);
        if (Objects.nonNull(caffeineCache)) {
            log.info("get data from caffeine");
            return caffeineCache;
        }
 
        //查詢Redis
        Object redisCache = redisTemplate.opsForValue().get(realKey);
        if (Objects.nonNull(redisCache)) {
            log.info("get data from redis");
            cache.put(realKey, redisCache);
            return redisCache;
        }
 
        log.info("get data from database");
        Object object = point.proceed();
        if (Objects.nonNull(object)){
            //寫入Redis
            redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
            //寫入Caffeine
            cache.put(realKey, object);        
        }
        return object;
    }
}

切面中主要做了下面幾件工作:

  • 通過方法的參數,解析注解中key的springEl表達式,組裝真正緩存的key。
  • 根據操作緩存的類型,分別處理存取、只存、刪除緩存操作。
  • 刪除和強制更新緩存的操作,都需要執(zhí)行原方法,并進行相應的緩存刪除或更新操作。
  • 存取操作前,先檢查緩存中是否有數據,如果有則直接返回,沒有則執(zhí)行原方法,并將結果存入緩存。

修改Service層代碼,代碼中只保留原有業(yè)務代碼,再添加上我們自定義的注解就可以了:

@DoubleCache(cacheName = "order", key = "#id",
        type = CacheType.FULL)
public Order getOrderById(Long id) {
    Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
            .eq(Order::getId, id));
    return myOrder;
}
 
@DoubleCache(cacheName = "order",key = "#order.id",
        type = CacheType.PUT)
public Order updateOrder(Order order) {
    orderMapper.updateById(order);
    return order;
}
 
@DoubleCache(cacheName = "order",key = "#id",
        type = CacheType.DELETE)
public void deleteOrder(Long id) {
    orderMapper.deleteById(id);
}

到這里,基于切面操作緩存的改造就完成了,Service的代碼也瞬間清爽了很多,讓我們可以繼續(xù)專注于業(yè)務邏輯處理,而不用費心去操作兩級緩存了。

總結本文按照對業(yè)務入侵的遞減程度,依次介紹了三種管理兩級緩存的方法。至于在項目中是否需要使用二級緩存,需要考慮自身業(yè)務情況,如果Redis這種遠程緩存已經能夠滿足你的業(yè)務需求,那么就沒有必要再使用本地緩存了。畢竟實際使用起來遠沒有那么簡單,本文中只是介紹了最基礎的使用,實際中的并發(fā)問題、事務的回滾問題都需要考慮,還需要思考什么數據適合放在一級緩存、什么數據適合放在二級緩存等等的其他問題。

到此這篇關于Redis+Caffeine兩級緩存的實現的文章就介紹到這了,更多相關Redis Caffeine兩級緩存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 基于Redis實現分布式鎖以及任務隊列

    基于Redis實現分布式鎖以及任務隊列

    這篇文章主要介紹了基于Redis實現分布式鎖以及任務隊列,需要的朋友可以參考下
    2015-11-11
  • Redis實現數據的交集、并集、補集的示例

    Redis實現數據的交集、并集、補集的示例

    本文主要介紹了Redis實現數據的交集、并集、補集的示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-08-08
  • 如何在centos中安裝redis插件bloom-filter

    如何在centos中安裝redis插件bloom-filter

    布隆過濾器在第一次add的時候自動創(chuàng)建基于默認參數的過濾器,Redis還提供了自定義參數的布隆過濾器,下面這篇文章主要給大家介紹了關于如何在centos中安裝redis插件bloom-filter的相關資料,需要的朋友可以參考下
    2021-11-11
  • Redis如何部署哨兵

    Redis如何部署哨兵

    本文主要介紹了Redis如何部署哨兵,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-07-07
  • Redis?異常?read?error?on?connection?的解決方案

    Redis?異常?read?error?on?connection?的解決方案

    這篇文章主要介紹了Redis異常read?error?on?connection的解決方案,文章圍繞主題展開詳細的內容介紹,具有一定的參考價值,感興趣的小伙伴可以參考一下
    2022-08-08
  • Redis 使用跳表實現有序集合的方法

    Redis 使用跳表實現有序集合的方法

    Redis有序集合底層為什么使用跳表而非其他數據結構如平衡樹、紅黑樹或B+樹的原因在于其特殊的設計和應用場景,跳表提供了與平衡樹類似的效率,同時實現更簡單,調試和修改也更加容易,感興趣的朋友一起看看吧
    2024-09-09
  • Redis過期鍵與內存淘汰策略深入分析講解

    Redis過期鍵與內存淘汰策略深入分析講解

    因為redis數據是基于內存的,然而內存是非常寶貴的資源,然后我們就會對一些不常用或者只用一次的數據進行存活時間設置,這樣才能提高內存的使用效率,下面這篇文章主要給大家介紹了關于Redis中過期鍵與內存淘汰策略,需要的朋友可以參考下
    2022-11-11
  • Redis調用Lua腳本及使用場景快速掌握

    Redis調用Lua腳本及使用場景快速掌握

    Redis?是一種非常流行的內存數據庫,常用于數據緩存與高頻數據存儲。大多數開發(fā)人員可能聽說過redis可以運行?Lua?腳本,但是可能不知道redis在什么情況下需要使用到Lua腳本
    2022-03-03
  • python腳本實現Redis未授權批量提權

    python腳本實現Redis未授權批量提權

    這篇文章主要給大家介紹了關于利用python腳本實現redis未授權批量提權的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
    2017-09-09
  • Linux中Redis安裝部署的操作步驟

    Linux中Redis安裝部署的操作步驟

    公司一直在使用redis集群,尋思著自己也部署一套練練手,下面這篇文章主要給大家介紹了關于Linux中Redis安裝部署的操作步驟,需要的朋友可以參考下
    2022-04-04

最新評論