模板方法模式实现Redis缓存查询简化

4706

简述

在高并发场景下查询缓存时很容易出现缓存击穿(本文针对单机没有使用分布式锁),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,因此查询缓存需要进行加锁,但这种代码每次写多了很烦,而且容易写错,因此本文采用模板方法模式简化缓存查询及并发处理

快速开始

Redis Dependency:

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

Redis Service

public interface RedisService {
    /**
     * 存储数据
     */
    void set(String key, String value);

    /**
     * SET key value EX seconds,设置key-value和超时时间(秒)
     * 存储数据并设置过期时间
     *
     * @param key cache key
     * @param value cache value
     * @param expire expire time,time unit is seconds
     */
    void set(String key, String value, long expire);

    /**
     * 存储数据并设置过期时间通知指定过期时间的单位
     * @param key 数据的键
     * @param value 值
     * @param expire 过期时间
     * @param timeUnit 过期时间的单位
     */
    void set(String key, String value, long expire, TimeUnit timeUnit);

    /**
     * 获取数据
     */
    String get(String key);

    /**
     * 设置超期时间
     * @param key
     * @param expire 过期时间
     * @return 设置成功返回true, 否则返回false
     */
    boolean expire(String key, long expire);

    /**
     * 删除数据
     */
    void remove(String key);

    /**
     * 自增操作
     * @param delta 自增步长
     */
    Long increment(String key, long delta);
}

Redis Service Impl

@Service
public class RedisServiceImpl implements RedisService {
    private static final long DEFAULT_EXPIRE_SECONDS = 10;

    private final StringRedisTemplate redisTemplate;

    @Autowired
    public RedisServiceImpl(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value, DEFAULT_EXPIRE_SECONDS, TimeUnit.MINUTES);
    }

    @Override
    public void set(String key, String value, long expire) {
        redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
    }

    @Override
    public void set(String key, String value, long expire, TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, expire, timeUnit);
    }

    @Override
    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    @Override
    public boolean expire(String key, long expire) {
        Boolean result = redisTemplate.expire(key, expire, TimeUnit.SECONDS);
        if(result == null) {
            return false;
        }
        return result;
    }

    @Override
    public void remove(String key) {
        redisTemplate.delete(key);
    }

    @Override
    public Long increment(String key, long delta) {
        return redisTemplate.opsForValue().increment(key,delta);
    }
}

Redis Key Util:

public class RedisUtils {
    /**
     * 主数据系统标识
     */
    private static final String KEY_PREFIX = "halo";
    /**
     * 分割字符,默认[:],使用:可用于rdm分组查看
     */
    private static final String KEY_SPLIT_CHAR = ":";

    /**
     * redis的key键规则定义
     * @param module 模块名称
     * @param service service名称
     * @param args 参数..
     * @return key
     */
    public static String keyBuilder(String module, String service, String... args) {
        return keyBuilder(null, module, service, args);
    }

    /**
     * redis的key键规则定义
     * @param module 模块名称
     * @param service service名称
     * @param objStr 对象.toString()
     * @return key
     */
    public static String keyBuilder(String module, String service, String objStr) {
        return keyBuilder(null, module, service, new String[]{objStr});
    }

    /**
     * redis的key键规则定义
     * @param prefix 项目前缀
     * @param module 模块名称
     * @param service service名称
     * @param objStr 对象.toString()
     * @return key
     */
    public static String keyBuilder(String prefix, String module, String service, String objStr) {
        return keyBuilder(prefix, module, service, new String[]{objStr});
    }

    /**
     * redis的key键规则定义
     * @param prefix 项目前缀
     * @param module 模块名称
     * @param service 方法名称
     * @param args 参数..
     * @return key
     */
    public static String keyBuilder(String prefix, String module, String service, String... args) {
        // 项目前缀
        if (prefix == null) {
            prefix = KEY_PREFIX;
        }
        StringBuilder key = new StringBuilder(prefix);
        // KEY_SPLIT_CHAR 为分割字符
        key.append(KEY_SPLIT_CHAR).append(module).append(KEY_SPLIT_CHAR).append(service);
        for (String arg : args) {
            key.append(KEY_SPLIT_CHAR).append(arg);
        }
        return key.toString();
    }

    /**
     * redis的key键规则定义
     * @param cacheEnum 枚举对象
     * @param objStr 对象.toString()
     * @return key
     */
    public static String keyBuilder(CacheEnum cacheEnum, String objStr) {
        return keyBuilder(cacheEnum.getPrefix(), cacheEnum.getModule(), cacheEnum.getService(), objStr);
    }
}

Cache Enum

@Data
@AllArgsConstructor
public enum CacheEnum {
    CMS_POST("halo", "cms", "PostService", "文章缓存");
    //other enum....
    
   /**
     * key前缀
     */
    private String prefix;
    /**
     * 模块名称
     */
    private String module;
    /**
     * service名称
     */
    private String service;
    /**
     * 描述
     */
    private String desc;
}

CacheTemplate:


@Component
public class CacheTemplate {
    @Autowired
    private RedisService redisService;

    public <T> T findCache(String key, Class<T> clazz, LoadCallback<T> loadCallback) {
        String cacheValueString =  redisService.get(key);
        if(StringUtils.isEmpty(cacheValueString)) {
            synchronized (this) {
                cacheValueString =  redisService.get(key);
                if(ObjectUtils.isEmpty(cacheValueString)) {
                    T dataOfMysql = loadCallback.load();

                    // 如果为null也放入缓存,过期时间为1分钟
                    if (dataOfMysql == null) {
                        redisService.set(key, "null", 60L);
                    } else {
                        // 否则放入缓存,过期时间为10分钟
                        redisService.set(key, JSON.toJSONString(dataOfMysql));
                    }

                    return dataOfMysql;
                }

                return JSON.parseObject(cacheValueString, clazz);
            }
        } else {
            return JSON.parseObject(cacheValueString, clazz);
        }
    }

    public <T> T findCache(String key, TypeReference<T> typeReference, LoadCallback<T> loadCallback) {
        String cacheValueString =  redisService.get(key);
        if(StringUtils.isEmpty(cacheValueString)) {
            synchronized (this) {
                cacheValueString =  redisService.get(key);
                if(ObjectUtils.isEmpty(cacheValueString)) {
                    T dataOfMysql = loadCallback.load();
                    // 如果为null也放入缓存,过期时间为1分钟
                    if (dataOfMysql == null) {
                        redisService.set(key, "null", 60L);
                    } else {
                        // 否则放入缓存,过期时间为10分钟
                        redisService.set(key, JSON.toJSONString(dataOfMysql));
                    }

                    return dataOfMysql;
                }

                return JSON.parseObject(cacheValueString, typeReference);
            }
        } else {
            return JSON.parseObject(cacheValueString, typeReference);
        }
    }

    public <T> T findCache(String key, long expire, TypeReference<T> typeReference, LoadCallback<T> loadCallback) {
        String cacheValueString =  redisService.get(key);

        if(StringUtils.isEmpty(cacheValueString)) {
            synchronized (this) {
                cacheValueString =  redisService.get(key);
                if(ObjectUtils.isEmpty(cacheValueString)) {
                    T dataOfMysql = loadCallback.load();

                    // 如果为null也放入缓存,过期时间为1分钟
                    if (dataOfMysql == null) {
                        redisService.set(key, "null", 60L);
                    } else {
                        // 否则放入缓存,过期时间为10分钟
                        redisService.set(key, JSON.toJSONString(dataOfMysql), expire);
                    }

                    return dataOfMysql;
                }

                return JSON.parseObject(cacheValueString, typeReference);
            }
        } else {
            return JSON.parseObject(cacheValueString, typeReference);
        }
    }
}

LoadCallback

@FunctionalInterface
public interface LoadCallback<T> {
    /**
     * 从数据库查询数据逻辑,查询后的结果会被优先放入缓存
     * @return 返回查询结果对象
     */
    T load();
}

Example:

@Slf4j
@Service
public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostService {
    private final PostRepository postRepository;
    private final CacheTemplate cacheTemplate;
    
    @Autowired
    public PostServiceImpl(PostRepository postRepository,
                          CacheTemplate cacheTemplate) {
        this.postRepository = postRepository;
        this.cacheTemplate = cacheTemplate;
    }
    
     public Post getBy(PostStatus status, String slug) {
         // 构建缓存key
         String cacheKey = RedisUtils.keyBuilder(CacheEnum.CMS_POST, "status:" + status.getValue() + ":slug:" + slug);
         return cacheTemplate.findCache(cacheKey, Post.class, ()->{
             // 从数据库查询数据的逻辑,这里可以简化为一行写
             return super.getBy(status, slug);
         });
    }
}