Springboot根据配置选择使用redis或内存做为应用的缓存工具

319 阅读4分钟

项目中使用到缓存的地方有很多,最常见的如用户信息的缓存,用户登录后对token、权限、菜单等信息的缓存,不必每次请求数据库造成请求缓慢且增加数据库压力。那么,常用缓存用那些呢?在这里提供两种,一是Map,另一个是Redis,使用方式不再赘述,后面会附上两种缓存的基本使用工具类。今天主要解决一个问题就是:如果我的项目里想做到==原本使用Map作为缓存工具,但因为Map在项目重启后会丢失数据想切换成Redis做为缓存工具,或者说我默认使用Map做为缓存工具,但要在我配置了Redis信息后自动使用Redis做为缓存工具该如何实现呢?

这里提供一种思路: 定义缓存工具类接口,两个缓存工具类(MapCacheUtil和RedisCacheUtil)分别实现缓存工具类接口,利用Springboot的注解@ConditionalOnExpression和@ConditionalOnMissingBean来根据配置自动决定使用Map或Redis做为缓存工具。

具体实现如下:

  1. 缓存工具类接口:
public interface CacheUtil {
    Long MAX_VALUE = (long) Integer.MAX_VALUE;
    TimeUnit DEFAULT_UNIT = TimeUnit.DAYS;

    /**
     * 设置缓存
     *
     * @param key   键
     * @param value 值
     * @return 是否成功
     */
    boolean set(String key, Object value);

    /**
     * 设置带过期时间缓存
     *
     * @param key      键
     * @param value    值
     * @param timeout  时间
     * @param timeUnit 时间单位
     * @return 是否成功
     */
    boolean set(String key, Object value, Long timeout, TimeUnit timeUnit);

    /**
     * 获取一个缓存
     *
     * @param key 键
     * @return 缓存数据
     */
    <T >T get(String key);

    /**
     * 删除一个缓存
     *
     * @param key 键
     * @return 是否删除成功
     */
    boolean remove(String key);

    /**
     * 判断是否存在缓存
     *
     * @param key 键
     * @return 是否存在
     */
    boolean hasKey(String key);
}

这里定义缓存工具类的基本功能清单

  1. Map缓存工具类
@Component
@Slf4j
@ConditionalOnMissingBean(value = RedisCacheUtil.class)
public class MapCacheUtil implements CacheUtil{
    private static final Map<String, CacheData> map = new ConcurrentHashMap<>();

    // 定期清除过期的key,key的过期时间最大有5S误差
    static {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                long currentTimeMillis = System.currentTimeMillis();
                for (String key : map.keySet()) {
                    CacheData cacheData = map.get(key);
                    if (cacheData == null || currentTimeMillis - cacheData.getLastVisitTime() < (cacheData.getTimeUnit().toSeconds(cacheData.getSeconds()) * 1000)) {
                        continue;
                    }
                    map.remove(key);
                    log.info("缓存过期,清理缓存:{}", key);
                }
            }
        }, 0, 5000);
    }

    /**
     * 设置缓存
     *
     * @param key   键
     * @param value 值
     * @return 是否成功
     */
    @Override
    public boolean set(String key, Object value) {
        return set(key, value, MAX_VALUE, DEFAULT_UNIT);
    }

    /**
     * 设置带过期时间缓存
     *
     * @param key      键
     * @param value    值
     * @param timeout  时间
     * @param timeUnit 时间单位
     * @return 是否成功
     */
    @Override
    public boolean set(String key, Object value, Long timeout, TimeUnit timeUnit) {
        if (StringUtils.isBlank(key)) {
            return false;
        }
        if (timeout == 0) return true;
        if (timeUnit == null) timeUnit = TimeUnit.SECONDS;
        CacheData cacheData = new CacheData(timeout, timeUnit, value);
        map.put(key, cacheData);
        return true;
    }

    /**
     * 获取一个缓存
     *
     * @param key 键
     * @return 缓存数据
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T>T get(String key) {
        if (StringUtils.isBlank(key)) return null;
        CacheData cacheData = map.get(key);
        if (cacheData == null) return null;
        return (T)cacheData.getData();
    }

    /**
     * 删除一个缓存
     *
     * @param key 键
     * @return 是否删除成功
     */
    @Override
    public boolean remove(String key) {
        if (StringUtils.isBlank(key)) return false;
        if (map.get(key) != null) {
            map.remove(key);
            return true;
        }
        return false;
    }

    /**
     * 判断是否存在缓存
     *
     * @param key 键
     * @return 是否存在
     */
    @Override
    public boolean hasKey(String key) {
        return map.get(key) != null;
    }

    private static class CacheData {
        /**
         * 缓存时长 秒
         */
        private Long seconds;
        private TimeUnit timeUnit;
        private Long lastVisitTime;
        private Object data;

        public CacheData(long seconds, TimeUnit timeUnit, Object data) {
            this.data = data;
            this.seconds = seconds;
            this.timeUnit = timeUnit;
            this.lastVisitTime = System.currentTimeMillis();
        }

        public Long getSeconds() {
            return seconds;
        }

        public void setSeconds(Long seconds) {
            this.seconds = seconds;
        }

        public Long getLastVisitTime() {
            return lastVisitTime;
        }

        public void setLastVisitTime(Long lastVisitTime) {
            this.lastVisitTime = lastVisitTime;
        }

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public TimeUnit getTimeUnit() {
            return timeUnit;
        }

        public void setTimeUnit(TimeUnit timeUnit) {
            this.timeUnit = timeUnit;
        }
    }
}

说明:

  • @ConditionalOnMissingBean(value = RedisCacheUtil.class) 如果环境中没有RedisCacheUtil这个bean,当前类才会被Springboot加载
  1. Redis缓存工具类
@Component
@RequiredArgsConstructor
@ConditionalOnExpression("#{environment['spring.redis.host']!=null or environment['spring.redis.cluster.nodes']!=null}")
public class RedisCacheUtil implements CacheUtil {
    private final RedisTemplate<String, Object> redisTemplate;

    /**
     * 设置缓存
     *
     * @param key   键
     * @param value 值
     * @return 是否成功
     */
    @Override
    public boolean set(String key, Object value) {
        if (StringUtils.isBlank(key) || value == null) return false;
        redisTemplate.opsForValue().set(key, value);
        return true;
    }

    /**
     * 设置带过期时间缓存
     *
     * @param key      键
     * @param value    值
     * @param timeout  时间
     * @param timeUnit 时间单位
     * @return 是否成功
     */
    @Override
    public boolean set(String key, Object value, Long timeout, TimeUnit timeUnit) {
        if (StringUtils.isBlank(key) || value == null) return false;
        if (timeUnit == null) timeUnit = TimeUnit.SECONDS;
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
        return true;
    }

    /**
     * 获取一个缓存
     *
     * @param key 键
     * @return 缓存数据
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T> T get(String key) {
        if (StringUtils.isBlank(key)) return null;
        Object o = redisTemplate.opsForValue().get(key);
        if (o == null) return null;
        return (T) o;

    }

    /**
     * 删除一个缓存
     *
     * @param key 键
     * @return 是否删除成功
     */
    @Override
    @SuppressWarnings("all")
    public boolean remove(String key) {
        if (StringUtils.isBlank(key)) return false;
        return redisTemplate.delete(key);
    }

    /**
     * 判断是否存在缓存
     *
     * @param key 键
     * @return 是否存在
     */
    @Override
    @SuppressWarnings("all")
    public boolean hasKey(String key) {
        if (StringUtils.isBlank(key)) return false;
        return redisTemplate.hasKey(key);
    }
}

说明:

  • @ConditionalOnExpression("#{environment['spring.redis.host']!=null or environment['spring.redis.cluster.nodes']!=null}") 当在配置文件中配置spring.redis.host或spring.redis.cluster.nodes时,当前类会被加载为bean

通过@ConditionalOnExpression和@ConditionalOnMissingBean这两个注解,实现了在未配置redis连接信息时就是用Map做为缓存工具,配置了redis连接信息后就是用Redis做为缓存工具

@ConditionalOnExpression为什么判断了spring.redis.host和spring.redis.cluster.nodes呢?

spring.redis.host用于单机redis的连接,spring.redis.cluster.nodes用于集群的连接,这样配置可支持redis集群

使用:

@RestController
@RequiredArgsConstructor
@Slf4j
public class TestCacheController {
    private final CacheUtil cacheUtil;

    @GetMapping("/testCache")
    public String testCache(){
        cacheUtil.set(UUID.randomUUID().toString(),"测试");
        cacheUtil.set(UUID.randomUUID().toString(),123);
        log.info("设置过期key...");
        cacheUtil.set("cc",123456, 10L,null);
        Integer cc=cacheUtil.get("cc");
        return "success";
    }
}

注意private final CacheUtil cacheUtil;的写法,用的是接口而不是具体的实现的构造器注入,也可使用@Autowire或@Resource自动注入