Spring Boot缓存应用实践

缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。

本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统。

一、通用缓存接口

1、缓存基础算法

(1)、FIFO(First In First Out),先进先出,和 OS 里的 FIFO 思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。
(2)、LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
(3)、LRU(Least Recently Used),最近最少使用,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据移除。

2、接口定义

简单定义缓存接口,大致可以抽象如下:

package com.power.demo.cache.contract;

import java.util.function.Function;

/**

  • 缓存提供者接口
    /
    public interface CacheProviderService {

    /**

    • 查询缓存
    • @param key 缓存键 不可为空
      /
      <T extends Object> T get(String key);

    /**

    • 查询缓存
    • @param key 缓存键 不可为空
    • @param function 如没有缓存,调用该 callable 函数返回对象 可为空
      /
      <T extends Object> T get(String key, Function<String, T> function);

    /**

    • 查询缓存
    • @param key 缓存键 不可为空
    • @param function 如没有缓存,调用该 callable 函数返回对象 可为空
    • @param funcParm function 函数的调用参数
      /
      <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm);

    /**

    • 查询缓存
    • @param key 缓存键 不可为空
    • @param function 如没有缓存,调用该 callable 函数返回对象 可为空
    • @param expireTime 过期时间(单位:毫秒) 可为空
      /
      <T extends Object> T get(String key, Function<String, T> function, Long expireTime);

    /**

    • 查询缓存
    • @param key 缓存键 不可为空
    • @param function 如没有缓存,调用该 callable 函数返回对象 可为空
    • @param funcParm function 函数的调用参数
    • @param expireTime 过期时间(单位:毫秒) 可为空
      /
      <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime);

    /**

    • 设置缓存键值
    • @param key 缓存键 不可为空
    • @param obj 缓存值 不可为空
      /
      <T extends Object> void set(String key, T obj);

    /**

    • 设置缓存键值
    • @param key 缓存键 不可为空
    • @param obj 缓存值 不可为空
    • @param expireTime 过期时间(单位:毫秒) 可为空
      /
      <T extends Object> void set(String key, T obj, Long expireTime);

    /**

    • 移除缓存
    • @param key 缓存键 不可为空
      /
      void remove(String key);

    /**

    • 是否存在缓存
    • @param key 缓存键 不可为空
      /
      boolean contains(String key);
      }
CacheProviderService

注意,这里列出的只是常见缓存功能接口,一些在特殊场景下用到的统计类的接口、分布式锁、自增(减)等功能不在讨论范围之内。

Get 相关方法,注意多个参数的情况,缓存接口里面传人的 Function,这是 Java8 提供的函数式接口,虽然支持的入参个数有限(这里你会非常怀念.NET 下的 Func 委托),但是仅对 Java 这个语言来说,这真是一个重大的进步 ^_^。

接口定义好了,下面就要实现缓存提供者程序了。按照存储类型的不同,本文简单实现最常用的两种缓存提供者:本地缓存和分布式缓存。

二、本地缓存

本地缓存,也就是 JVM 级别的缓存 (本地缓存可以认为是直接在进程内通信调用,而分布式缓存则需要通过网络进行跨进程通信调用),一般有很多种实现方式,比如直接使用 Hashtable、ConcurrentHashMap 等天生线程安全的集合作为缓存容器,或者使用一些成熟的开源组件,如 EhCache、Guava Cache 等。本文选择上手简单的 Guava 缓存。

1、什么是 Guava

Guava,简单来说就是一个开发类库,且是一个非常丰富强大的开发工具包,号称可以让使用 Java 语言更令人愉悦,主要包括基本工具类库和接口、缓存、发布订阅风格的事件总线等。在实际开发中,我用的最多的是集合、缓存和常用类型帮助类,很多人都对这个类库称赞有加。

2、添加依赖

     <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
google.guava

3、实现接口

package com.power.demo.cache.impl;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;

/*

  • 本地缓存提供者服务 (Guava Cache)

  • */
    @Configuration
    @ComponentScan(basePackages
    = AppConst.BASE_PACKAGE_NAME)
    @Qualifier(
    "localCacheService")
    public class LocalCacheProviderImpl implements CacheProviderService {

    private static Map<String, Cache<String, Object>> _cacheMap = Maps.newConcurrentMap();

    static {

     Cache</span>&lt;String, Object&gt; cacheContainer =<span style="color: rgba(0, 0, 0, 1)"> CacheBuilder.newBuilder()
             .maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
             .expireAfterWrite(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">最后一次写入后的一段时间移出
             </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">最后一次访问后的一段时间移出</span>
             .recordStats()<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">开启统计功能</span>
    

.build();

    _cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE), cacheContainer);
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt;<span style="color: rgba(0, 0, 0, 1)"> T get(String key) {
    T obj </span>= get(key, <span style="color: rgba(0, 0, 255, 1)">null</span>, <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, AppConst.CACHE_MINUTE);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key      缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function 如没有缓存,调用该callable函数返回对象 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;String, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function) {
    T obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key, function, key, AppConst.CACHE_MINUTE);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key      缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function 如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> funcParm function函数的调用参数
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object, M <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;M, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function, M funcParm) {
    T obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key, function, funcParm, AppConst.CACHE_MINUTE);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function   如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;String, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function, Long expireTime) {
    T obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key, function, key, expireTime);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function   如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> funcParm   function函数的调用参数
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object, M <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;M, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function, M funcParm, Long expireTime) {
    T obj </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
    }

    expireTime </span>=<span style="color: rgba(0, 0, 0, 1)"> getExpireTime(expireTime);

    Cache</span>&lt;String, Object&gt; cacheContainer =<span style="color: rgba(0, 0, 0, 1)"> getCacheContainer(expireTime);

    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (function == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            obj </span>=<span style="color: rgba(0, 0, 0, 1)"> (T) cacheContainer.getIfPresent(key);
        } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
            </span><span style="color: rgba(0, 0, 255, 1)">final</span> Long cachedTime =<span style="color: rgba(0, 0, 0, 1)"> expireTime;
            obj </span>= (T) cacheContainer.get(key, () -&gt;<span style="color: rgba(0, 0, 0, 1)"> {
                T retObj </span>=<span style="color: rgba(0, 0, 0, 1)"> function.apply(funcParm);
                </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> retObj;
            });
        }
    } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) {
        e.printStackTrace();
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 设置缓存键值  直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> obj 缓存值 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> set(String key, T obj) {

    set(key, obj, AppConst.CACHE_MINUTE);
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 设置缓存键值  直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> obj        缓存值 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> set(String key, T obj, Long expireTime) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    expireTime </span>=<span style="color: rgba(0, 0, 0, 1)"> getExpireTime(expireTime);

    Cache</span>&lt;String, Object&gt; cacheContainer =<span style="color: rgba(0, 0, 0, 1)"> getCacheContainer(expireTime);

    cacheContainer.put(key, obj);
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 移除缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> remove(String key) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">long</span> expireTime =<span style="color: rgba(0, 0, 0, 1)"> getExpireTime(AppConst.CACHE_MINUTE);

    Cache</span>&lt;String, Object&gt; cacheContainer =<span style="color: rgba(0, 0, 0, 1)"> getCacheContainer(expireTime);

    cacheContainer.invalidate(key);
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 是否存在缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> contains(String key) {
    </span><span style="color: rgba(0, 0, 255, 1)">boolean</span> exists = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> exists;
    }

    Object obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key);

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
        exists </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> exists;
}

</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> Lock lock = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ReentrantLock();

</span><span style="color: rgba(0, 0, 255, 1)">private</span> Cache&lt;String, Object&gt;<span style="color: rgba(0, 0, 0, 1)"> getCacheContainer(Long expireTime) {

    Cache</span>&lt;String, Object&gt; cacheContainer = <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (expireTime == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> cacheContainer;
    }

    String mapKey </span>=<span style="color: rgba(0, 0, 0, 1)"> String.valueOf(expireTime);

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (_cacheMap.containsKey(mapKey) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        cacheContainer </span>=<span style="color: rgba(0, 0, 0, 1)"> _cacheMap.get(mapKey);
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> cacheContainer;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
        lock.lock();
        cacheContainer </span>=<span style="color: rgba(0, 0, 0, 1)"> CacheBuilder.newBuilder()
                .maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
                .expireAfterWrite(expireTime, TimeUnit.MILLISECONDS)</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">最后一次写入后的一段时间移出
                </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">最后一次访问后的一段时间移出</span>
                .recordStats()<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">开启统计功能</span>

.build();

        _cacheMap.put(mapKey, cacheContainer);

    } </span><span style="color: rgba(0, 0, 255, 1)">finally</span><span style="color: rgba(0, 0, 0, 1)"> {
        lock.unlock();
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> cacheContainer;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 获取过期时间 单位:毫秒
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 传人的过期时间 单位毫秒 如小于1分钟,默认为10分钟
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Long getExpireTime(Long expireTime) {
    Long result </span>=<span style="color: rgba(0, 0, 0, 1)"> expireTime;
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (expireTime == <span style="color: rgba(0, 0, 255, 1)">null</span> || expireTime &lt; AppConst.CACHE_MINUTE / 10<span style="color: rgba(0, 0, 0, 1)">) {
        result </span>=<span style="color: rgba(0, 0, 0, 1)"> AppConst.CACHE_MINUTE;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result;
}

}

LocalCacheProviderImpl

4、注意事项

Guava Cache 初始化容器时,支持缓存过期策略,类似 FIFO、LRU 和 LFU 等算法。

expireAfterWrite:最后一次写入后的一段时间移出。

expireAfterAccess:最后一次访问后的一段时间移出。

Guava Cache 对缓存过期时间的设置实在不够友好。常见的应用场景,比如,有些几乎不变的基础数据缓存 1 天,有些热点数据缓存 2 小时,有些会话数据缓存 5 分钟等等。

通常我们认为设置缓存的时候带上缓存的过期时间是非常容易的,而且只要一个缓存容器实例即可,比如.NET 下的 ObjectCache、System.Runtime.Cache 等等。

但是 Guava Cache 不是这个实现思路,如果缓存的过期时间不同,Guava 的 CacheBuilder 要初始化多份 Cache 实例。

好在我在实现的时候注意到了这个问题,并且提供了解决方案,可以看到 getCacheContainer 这个函数,根据过期时长做缓存实例判断,就算不同过期时间的多实例缓存也是完全没有问题的。

三、分布式缓存

分布式缓存产品非常多,本文使用应用普遍的 Redis,在 Spring Boot 应用中使用 Redis 非常简单。

1、什么是 Redis

 Redis 是一款开源(BSD 许可)的、用 C 语言写成的高性能的键 - 值存储(key-value store)。它常被称作是一款数据结构服务器(data structure server)。它可以被用作缓存、消息中间件和数据库,在很多应用中,经常看到有人选择使用 Redis 做缓存,实现分布式锁和分布式 Session 等。作为缓存系统时,和经典的 KV 结构的 Memcached 非常相似,但又有很多不同。
Redis 支持丰富的数据类型。Redis 的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等数据类型。 对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。

Redis 的数据类型
Keys: 非二进制安全的字符类型( not binary-safe strings ),由于 key 不是 binary safe 的字符串,所以像“my key”和“mykey\n”这样包含空格和换行的 key 是不允许的。
Values:Strings、Hash、Lists、 Sets、 Sorted sets。考虑到 Redis 单线程操作模式,Value 的粒度不应该过大,缓存的值越大,越容易造成阻塞和排队。


为了获得优异的性能,Redis 采用了内存中(in-memory)数据集(dataset)的方式。同时,Redis 支持数据的持久化,你可以每隔一段时间将数据集转存到磁盘上(snapshot),或者在日志尾部追加每一条操作命令(append only file,aof)。
Redis 同样支持主从复制(master-slave replication),并且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、网络断开自动重连等功能。
同时 Redis 还具有其它一些特性,其中包括简单的事物支持、发布订阅 ( pub/sub)、管道(pipeline)和虚拟内存(vm)等 。

2、添加依赖

   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
redis

3、配置 Redis

 在 application.properties 配置文件中,配置 Redis 常用参数:

## Redis 缓存相关配置
#Redis 数据库索引(默认为 0)
spring.redis.database=0
#Redis 服务器地址
spring.redis.host=127.0.0.1
#Redis 服务器端口
spring.redis.port=6379  
#Redis 服务器密码(默认为空)
spring.redis.password=123321
#Redis 连接超时时间 默认:5 分钟(单位:毫秒)
spring.redis.timeout=300000ms
#Redis 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=512
#Redis 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
#Redis 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
#Redis 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1ms
redisproperties

常见的需要注意的是最大连接数(spring.redis.jedis.pool.max-active )和超时时间(spring.redis.jedis.pool.max-wait)。Redis 在生产环境中出现故障的频率经常和这两个参数息息相关。

接着定义一个继承自 CachingConfigurerSupport(请注意 cacheManager 和 keyGenerator 这两个方法在子类的实现)的 RedisConfig 类:

package com.power.demo.cache.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**

  • Redis 缓存配置类
    */
    @Configuration
    @EnableCaching
    public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
    return RedisCacheManager.create(connectionFactory);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate
    <String, Object> template = new RedisTemplate<>();

     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Jedis的Key和Value的序列化器默认值是JdkSerializationRedisSerializer
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">经实验,JdkSerializationRedisSerializer通过RedisDesktopManager看到的键值对不能正常解析
    
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置key的序列化器</span>
     template.setKeySerializer(<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> StringRedisSerializer());
    
     </span><span style="color: rgba(0, 128, 0, 1)">////</span><span style="color: rgba(0, 128, 0, 1)">设置value的序列化器  默认值是JdkSerializationRedisSerializer
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">使用Jackson序列化器的问题是,复杂对象可能序列化失败,比如JodaTime的DateTime类型
    
     </span><span style="color: rgba(0, 128, 0, 1)">//</span>        <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">使用Jackson2,将对象序列化为JSON
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
     </span><span style="color: rgba(0, 128, 0, 1)">//</span>        <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">json转对象类,不设置默认的会将json转成hashmap
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">        ObjectMapper om = new ObjectMapper();
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">        jackson2JsonRedisSerializer.setObjectMapper(om);
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">        template.setValueSerializer(jackson2JsonRedisSerializer);
    
     </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">将redis连接工厂设置到模板类中</span>
    

template.setConnectionFactory(factory);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> template;
}

// //自定义缓存 key 生成策略
// @Bean
// public KeyGenerator keyGenerator() {
// return new KeyGenerator() {
// @Override
// public Object generate(Object target, java.lang.reflect.Method method, Object... params) {
// StringBuffer sb = new StringBuffer();
// sb.append(target.getClass().getName());
// sb.append(method.getName());
// for (Object obj : params) {
// if (obj == null) {
// continue;
// }
// sb.append(obj.toString());
// }
// return sb.toString();
// }
// };
// }
}

RedisConfig

在 RedisConfig 这个类上加上@EnableCaching这个注解,这个注解会被 Spring 发现,并且会创建一个切面(aspect) 并触发 Spring 缓存注解的切点(pointcut)。据所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值。
cacheManager方法,申明一个缓存管理器(CacheManager)的 bean,作用就是 @EnableCaching 这个切面在新增缓存或者删除缓存的时候会调用这个缓存管理器的方法。keyGenerator方法,可以根据需求自定义缓存 key 生成策略。

而 redisTemplate 方法,则主要是设置 Redis 模板类,比如键和值的序列化器(从这里可以看出,Redis 的键值对必须可序列化)、redis 连接工厂等。

RedisTemplate 支持的序列化器主要有如下几种:

JdkSerializationRedisSerializer:使用 Java 序列化;
StringRedisSerializer:序列化 String 类型的 key 和 value;
GenericToStringSerializer:使用 Spring 转换服务进行序列化;
JacksonJsonRedisSerializer:使用 Jackson 1,将对象序列化为 JSON;
Jackson2JsonRedisSerializer:使用 Jackson 2,将对象序列化为 JSON;
OxmSerializer:使用 Spring O/X 映射的编排器和解排器(marshaler 和 unmarshaler)实现序列化,用于 XML 序列化;

注意:RedisTemplate 的键和值序列化器,默认情况下都是 JdkSerializationRedisSerializer,它们都可以自定义设置序列化器。推荐将字符串键使用 StringRedisSerializer 序列化器,因为运维的时候好排查问题,JDK 序列化器的也能识别,但是可读性稍差 (是因为缓存服务器没有 JRE 吗?),见如下效果:

而值序列化器则要复杂的多,很多人推荐使用 Jackson2JsonRedisSerializer 序列化器,但是实际开发过程中,经常有人碰到反序列化错误,经过排查多数都和 Jackson2JsonRedisSerializer 这个序列化器有关。

4、实现接口

使用 RedisTemplate,在 Spring Boot 中调用 Redis 接口比直接调用 Jedis 简单多了。

package com.power.demo.cache.impl;

import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Configuration
@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
@Qualifier(
"redisCacheService")
public class RedisCacheProviderImpl implements CacheProviderService {

@Resource
</span><span style="color: rgba(0, 0, 255, 1)">private</span> RedisTemplate&lt;Serializable, Object&gt;<span style="color: rgba(0, 0, 0, 1)"> redisTemplate;

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt;<span style="color: rgba(0, 0, 0, 1)"> T get(String key) {
    T obj </span>= get(key, <span style="color: rgba(0, 0, 255, 1)">null</span>, <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, AppConst.CACHE_MINUTE);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key      缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function 如没有缓存,调用该callable函数返回对象 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;String, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function) {
    T obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key, function, key, AppConst.CACHE_MINUTE);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key      缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function 如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> funcParm function函数的调用参数
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object, M <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;M, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function, M funcParm) {
    T obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key, function, funcParm, AppConst.CACHE_MINUTE);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function   如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;String, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function, Long expireTime) {
    T obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key, function, key, expireTime);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function   如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> funcParm   function函数的调用参数
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object, M <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;M, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function, M funcParm, Long expireTime) {
    T obj </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
    }

    expireTime </span>=<span style="color: rgba(0, 0, 0, 1)"> getExpireTime(expireTime);

    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {

        ValueOperations</span>&lt;Serializable, Object&gt; operations =<span style="color: rgba(0, 0, 0, 1)"> redisTemplate.opsForValue();
        obj </span>=<span style="color: rgba(0, 0, 0, 1)"> (T) operations.get(key);
        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (function != <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            obj </span>=<span style="color: rgba(0, 0, 0, 1)"> function.apply(funcParm);
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
                set(key, obj, expireTime);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置缓存信息</span>

}
}
}
catch (Exception e) {
e.printStackTrace();
}

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 设置缓存键值  直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> obj 缓存值 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> set(String key, T obj) {

    set(key, obj, AppConst.CACHE_MINUTE);
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 设置缓存键值  直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> obj        缓存值 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> set(String key, T obj, Long expireTime) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    expireTime </span>=<span style="color: rgba(0, 0, 0, 1)"> getExpireTime(expireTime);

    ValueOperations</span>&lt;Serializable, Object&gt; operations =<span style="color: rgba(0, 0, 0, 1)"> redisTemplate.opsForValue();

    operations.set(key, obj);

    redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS);
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 移除缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> remove(String key) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    redisTemplate.delete(key);
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 是否存在缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> contains(String key) {
    </span><span style="color: rgba(0, 0, 255, 1)">boolean</span> exists = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> exists;
    }

    Object obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key);

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
        exists </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> exists;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 获取过期时间 单位:毫秒
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 传人的过期时间 单位毫秒 如小于1分钟,默认为10分钟
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Long getExpireTime(Long expireTime) {
    Long result </span>=<span style="color: rgba(0, 0, 0, 1)"> expireTime;
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (expireTime == <span style="color: rgba(0, 0, 255, 1)">null</span> || expireTime &lt; AppConst.CACHE_MINUTE / 10<span style="color: rgba(0, 0, 0, 1)">) {
        result </span>=<span style="color: rgba(0, 0, 0, 1)"> AppConst.CACHE_MINUTE;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result;
}

}

RedisCacheProviderImpl

注意:很多教程里都讲到通过注解的方式(@Cacheable,@CachePut、@CacheEvict 和 @Caching)实现数据缓存,根据实践,我个人是不推崇这种使用方式的。

四、缓存“及时”过期问题

这个也是开发和运维过程中非常经典的问题。

有些公司写缓存客户端的时候,会给每个团队分别定义一个 Area,但是这个只能做到缓存键的分布区分,不能保证缓存“实时”有效的过期。

多年以前我写过一篇结合实际情况的文章,也就是加上缓存版本,请猛击这里 ,算是提供了一种相对有效的方案,不过高并发站点要慎重,防止发生雪崩效应。

Redis 还有一些其他常见问题,比如:Redis 的字符串类型 Key 和 Value 都有限制,且都是不能超过 512M,请猛击这里。还有最大连接数和超时时间设置等问题,本文就不再一一列举了。

五、二级缓存

在配置文件中,加上缓存提供者开关:

## 是否启用本地缓存
spring.power.isuselocalcache=1
## 是否启用 Redis 缓存
spring.power.isuserediscache=1
cache_switch

缓存提供者程序都实现好了,我们会再包装一个调用外观类 PowerCacheBuilder,加上缓存版本控制,可以轻松自如地控制和切换缓存,code talks:

package com.power.demo.cache;

import com.google.common.collect.Lists;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import com.power.demo.common.AppField;
import com.power.demo.util.ConfigUtil;
import com.power.demo.util.PowerLogger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;

/*

  • 支持多缓存提供程序多级缓存的缓存帮助类

  • */
    @Configuration
    @ComponentScan(basePackages
    = AppConst.BASE_PACKAGE_NAME)
    public class PowerCacheBuilder {

    @Autowired
    @Qualifier("localCacheService")
    private CacheProviderService localCacheService;

    @Autowired
    @Qualifier("redisCacheService")
    private CacheProviderService redisCacheService;

    private static List<CacheProviderService> _listCacheProvider = Lists.newArrayList();

    private static final Lock providerLock = new ReentrantLock();

    /**

    • 初始化缓存提供者 默认优先级:先本地缓存,后分布式缓存
      /
      private List<CacheProviderService> getCacheProviders() {

      if (_listCacheProvider.size() > 0) {
      return _listCacheProvider;
      }

      //线程安全
      try {
      providerLock.tryLock(
      1000, TimeUnit.MILLISECONDS);

       </span><span style="color: rgba(0, 0, 255, 1)">if</span> (_listCacheProvider.size() &gt; 0<span style="color: rgba(0, 0, 0, 1)">) {
           </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> _listCacheProvider;
       }
      
       String isUseCache </span>=<span style="color: rgba(0, 0, 0, 1)"> ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);
      
       CacheProviderService cacheProviderService </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
      
       </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">启用本地缓存</span>
       <span style="color: rgba(0, 0, 255, 1)">if</span> ("1"<span style="color: rgba(0, 0, 0, 1)">.equalsIgnoreCase(isUseCache)) {
           _listCacheProvider.add(localCacheService);
       }
      
       isUseCache </span>=<span style="color: rgba(0, 0, 0, 1)"> ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
      
       </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">启用Redis缓存</span>
       <span style="color: rgba(0, 0, 255, 1)">if</span> ("1"<span style="color: rgba(0, 0, 0, 1)">.equalsIgnoreCase(isUseCache)) {
           _listCacheProvider.add(redisCacheService);
      
           resetCacheVersion();</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置分布式缓存版本号</span>
      

}

        PowerLogger.info(</span>"初始化缓存提供者成功,共有" + _listCacheProvider.size() + "个"<span style="color: rgba(0, 0, 0, 1)">);
    } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) {
        e.printStackTrace();

        _listCacheProvider </span>=<span style="color: rgba(0, 0, 0, 1)"> Lists.newArrayList();

        PowerLogger.error(</span>"初始化缓存提供者发生异常:{}"<span style="color: rgba(0, 0, 0, 1)">, e);
    } </span><span style="color: rgba(0, 0, 255, 1)">finally</span><span style="color: rgba(0, 0, 0, 1)"> {
        providerLock.unlock();
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> _listCacheProvider;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt;<span style="color: rgba(0, 0, 0, 1)"> T get(String key) {
    T obj </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">key = generateVerKey(key);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">构造带版本的缓存键</span>

    <span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (CacheProviderService provider : getCacheProviders()) {

        obj </span>=<span style="color: rgba(0, 0, 0, 1)"> provider.get(key);

        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
        }
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key      缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function 如没有缓存,调用该callable函数返回对象 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;String, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function) {
    T obj </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;

    </span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (CacheProviderService provider : getCacheProviders()) {

        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            obj </span>=<span style="color: rgba(0, 0, 0, 1)"> provider.get(key, function);
        } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (function != <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj != <span style="color: rgba(0, 0, 255, 1)">null</span>) {<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">查询并设置其他缓存提供者程序缓存</span>

provider.get(key, function);
}

        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">如果callable函数为空 而缓存对象不为空 及时跳出循环并返回</span>
        <span style="color: rgba(0, 0, 255, 1)">if</span> (function == <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
        }

    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key      缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function 如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> funcParm function函数的调用参数
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object, M <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;M, T&gt;<span style="color: rgba(0, 0, 0, 1)"> function, M funcParm) {
    T obj </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;

    </span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (CacheProviderService provider : getCacheProviders()) {

        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            obj </span>=<span style="color: rgba(0, 0, 0, 1)"> provider.get(key, function, funcParm);
        } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (function != <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj != <span style="color: rgba(0, 0, 255, 1)">null</span>) {<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">查询并设置其他缓存提供者程序缓存</span>

provider.get(key, function, funcParm);
}

        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">如果callable函数为空 而缓存对象不为空 及时跳出循环并返回</span>
        <span style="color: rgba(0, 0, 255, 1)">if</span> (function == <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
        }
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function   如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;String, T&gt; function, <span style="color: rgba(0, 0, 255, 1)">long</span><span style="color: rgba(0, 0, 0, 1)"> expireTime) {
    T obj </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;

    </span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (CacheProviderService provider : getCacheProviders()) {

        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            obj </span>=<span style="color: rgba(0, 0, 0, 1)"> provider.get(key, function, expireTime);
        } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (function != <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj != <span style="color: rgba(0, 0, 255, 1)">null</span>) {<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">查询并设置其他缓存提供者程序缓存</span>

provider.get(key, function, expireTime);
}

        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">如果callable函数为空 而缓存对象不为空 及时跳出循环并返回</span>
        <span style="color: rgba(0, 0, 255, 1)">if</span> (function == <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
        }
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 查询缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> function   如没有缓存,调用该callable函数返回对象 可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> funcParm   function函数的调用参数
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object, M <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; T get(String key, Function&lt;M, T&gt; function, M funcParm, <span style="color: rgba(0, 0, 255, 1)">long</span><span style="color: rgba(0, 0, 0, 1)"> expireTime) {
    T obj </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;

    </span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (CacheProviderService provider : getCacheProviders()) {

        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            obj </span>=<span style="color: rgba(0, 0, 0, 1)"> provider.get(key, function, funcParm, expireTime);
        } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (function != <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj != <span style="color: rgba(0, 0, 255, 1)">null</span>) {<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">查询并设置其他缓存提供者程序缓存</span>

provider.get(key, function, funcParm, expireTime);
}

        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">如果callable函数为空 而缓存对象不为空 及时跳出循环并返回</span>
        <span style="color: rgba(0, 0, 255, 1)">if</span> (function == <span style="color: rgba(0, 0, 255, 1)">null</span> &amp;&amp; obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
        }
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> obj;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 设置缓存键值  直接向缓存中插入或覆盖值
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> obj 缓存值 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> set(String key, T obj) {

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">key = generateVerKey(key);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">构造带版本的缓存键</span>

    <span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (CacheProviderService provider : getCacheProviders()) {

        provider.set(key, obj);

    }
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 设置缓存键值  直接向缓存中插入或覆盖值
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key        缓存键 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> obj        缓存值 不可为空
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> expireTime 过期时间(单位:毫秒) 可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> &lt;T <span style="color: rgba(0, 0, 255, 1)">extends</span> Object&gt; <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> set(String key, T obj, Long expireTime) {

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">key = generateVerKey(key);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">构造带版本的缓存键</span>

    <span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (CacheProviderService provider : getCacheProviders()) {

        provider.set(key, obj, expireTime);

    }
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 移除缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> remove(String key) {

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">key = generateVerKey(key);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">构造带版本的缓存键</span>

    <span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (CacheProviderService provider : getCacheProviders()) {

        provider.remove(key);

    }
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 是否存在缓存
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> key 缓存键 不可为空
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> contains(String key) {
    </span><span style="color: rgba(0, 0, 255, 1)">boolean</span> exists = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">key = generateVerKey(key);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">构造带版本的缓存键</span>

    <span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> exists;
    }

    Object obj </span>=<span style="color: rgba(0, 0, 0, 1)"> get(key);

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (obj != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
        exists </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> exists;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 获取分布式缓存版本号
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getCacheVersion() {
    String version </span>= ""<span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">boolean</span> isUseCache =<span style="color: rgba(0, 0, 0, 1)"> checkUseRedisCache();

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">未启用Redis缓存</span>
    <span style="color: rgba(0, 0, 255, 1)">if</span> (isUseCache == <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> version;
    }

    version </span>=<span style="color: rgba(0, 0, 0, 1)"> redisCacheService.get(AppConst.CACHE_VERSION_KEY);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> version;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 重置分布式缓存版本  如果启用分布式缓存,设置缓存版本
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String resetCacheVersion() {
    String version </span>= ""<span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">boolean</span> isUseCache =<span style="color: rgba(0, 0, 0, 1)"> checkUseRedisCache();

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">未启用Redis缓存</span>
    <span style="color: rgba(0, 0, 255, 1)">if</span> (isUseCache == <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> version;
    }

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置缓存版本</span>
    version =<span style="color: rgba(0, 0, 0, 1)"> String.valueOf(Math.abs(UUID.randomUUID().hashCode()));
    redisCacheService.set(AppConst.CACHE_VERSION_KEY, version);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> version;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 如果启用分布式缓存,获取缓存版本,重置查询的缓存key,可以实现相对实时的缓存过期控制
 * &lt;p&gt;
 * 如没有启用分布式缓存,缓存key不做修改,直接返回
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String generateVerKey(String key) {

    String result </span>=<span style="color: rgba(0, 0, 0, 1)"> key;
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(key) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result;
    }

    </span><span style="color: rgba(0, 0, 255, 1)">boolean</span> isUseCache =<span style="color: rgba(0, 0, 0, 1)"> checkUseRedisCache();

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">没有启用分布式缓存,缓存key不做修改,直接返回</span>
    <span style="color: rgba(0, 0, 255, 1)">if</span> (isUseCache == <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result;
    }

    String version </span>=<span style="color: rgba(0, 0, 0, 1)"> redisCacheService.get(AppConst.CACHE_VERSION_KEY);
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isEmpty(version) == <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result;
    }

    result </span>= String.format("%s_%s"<span style="color: rgba(0, 0, 0, 1)">, result, version);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result;
}

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 验证是否启用分布式缓存
 *</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span><span style="color: rgba(0, 0, 0, 1)"> checkUseRedisCache() {
    </span><span style="color: rgba(0, 0, 255, 1)">boolean</span> isUseCache = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
    String strIsUseCache </span>=<span style="color: rgba(0, 0, 0, 1)"> ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);

    isUseCache </span>= "1"<span style="color: rgba(0, 0, 0, 1)">.equalsIgnoreCase(strIsUseCache);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> isUseCache;
}

}

PowerCacheBuilder

单元测试如下:

    @Test
    public void testCacheVerson() throws Exception {
    String version </span>=<span style="color: rgba(0, 0, 0, 1)"> cacheBuilder.getCacheVersion();
    System.out.println(String.format(</span>"当前缓存版本:%s"<span style="color: rgba(0, 0, 0, 1)">, version));

    String cacheKey </span>= cacheBuilder.generateVerKey("goods778899"<span style="color: rgba(0, 0, 0, 1)">);

    GoodsVO goodsVO </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> GoodsVO();
    goodsVO.setGoodsId(UUID.randomUUID().toString());
    goodsVO.setCreateTime(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Date());
    goodsVO.setCreateDate(</span><span style="color: rgba(0, 0, 255, 1)">new</span> DateTime(<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Date()));
    goodsVO.setGoodsType(</span>1024<span style="color: rgba(0, 0, 0, 1)">);
    goodsVO.setGoodsCode(</span>"123456789"<span style="color: rgba(0, 0, 0, 1)">);
    goodsVO.setGoodsName(</span>"我的测试商品"<span style="color: rgba(0, 0, 0, 1)">);

    cacheBuilder.set(cacheKey, goodsVO);

    GoodsVO goodsVO1 </span>=<span style="color: rgba(0, 0, 0, 1)"> cacheBuilder.get(cacheKey);

    Assert.assertNotNull(goodsVO1);

    version </span>=<span style="color: rgba(0, 0, 0, 1)"> cacheBuilder.resetCacheVersion();
    System.out.println(String.format(</span>"重置后的缓存版本:%s"<span style="color: rgba(0, 0, 0, 1)">, version));


    cacheKey </span>= cacheBuilder.generateVerKey("goods112233"<span style="color: rgba(0, 0, 0, 1)">);

    cacheBuilder.set(cacheKey, goodsVO);

    GoodsVO goodsVO2 </span>=<span style="color: rgba(0, 0, 0, 1)"> cacheBuilder.get(cacheKey);

    Assert.assertNotNull(goodsVO2);

    Assert.assertTrue(</span>"两个缓存对象的主键相同"<span style="color: rgba(0, 0, 0, 1)">, goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId()));
}</span></pre>
testCacheVerson

一个满足基本功能的多级缓存系统就好了。

在 Spring Boot 应用中使用缓存则非常简洁,选择调用上面包装好的缓存接口即可。

String cacheKey = _cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid." + request.getGoodsId());

GoodsVO goodsVO = _cacheBuilder.get(cacheKey, _goodsService::getGoodsByGoodsId, request.getGoodsId());

到这里 Spring Boot 业务系统开发中最常用到的 ORM,缓存和队列三板斧就介绍完了。

在开发的过程中你会发现,Java 真的是非常非常中规中矩的语言,你需要不断折腾并熟悉常见的开源中间件和工具,开源的轮子实在是太丰富,多尝试几个,实践出真知。

预告一下,后面的文章我将继续学习分享介绍常用中间件和工具,如定时任务,MongoDB,ES,分布式文件系统以及各种实用工具。总之 Java 下工具链非常完备,我们要给自己更多动力。

 

参考:

http://ifeve.com/google-guava/

http://www.cnblogs.com/luochengqiuse/p/4640932.html