【夯实Redis】如何使用内存缓存与Redis缓存实现多级缓存?

目录

多级缓存实现方案

参考实例代码


多级缓存实现方案

       首先看一下流程图。客户端在获取数据的时候,首先向当前服务所在内存请求缓存数据。如果内存中有缓存数据则直接返回缓存数据。如果没有内存缓存,则向分布式缓存Redis服务器请求数据。如果Redis中存在缓存,则将Redis中的缓存写入内存缓存,并向客户端返回缓存数据。如果Redis中也没有数据,那么只能查询数据库了。查询完数据,需要把此次查询结果分别写入Redis中 与 内存中。

        整体流程图如下,实现Java内存缓存与Redis缓存共存的多级缓存,可以提高系统的整体性能与并发能力。缺点是接入Redis缓存后,系统整体的可用性降低,复杂度升高。

        

参考实例代码

        首选创建缓存对象

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Accessors(chain = true)
public class CacheObject<T> {

    private T data;

    private boolean expired;

    public static <T> CacheObject<T> of(T data) {
        //测试使用:默认数据是过期的,模拟过期的数据被清除
        return new CacheObject<T>().setData(data).setExpired(true);
    }
}

        接下来创建一个MultiCache对象,并将其加入到Spring容器中。在该Bean中注入RedisTemplate,用于查询Redis。使用@PostConstruct在创建该Bean的时候为该Bean初始化一个守护线程,用于定时清理过期的缓存对象。这里的缓存对象使用了软引用包裹,它会在系统内存不足的时候强制回收掉对象所占内存。

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.lang.ref.SoftReference;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@Component
@RequiredArgsConstructor
public class MultiCache<T> {

    private final ConcurrentHashMap<String, SoftReference<CacheObject<T>>> cacheMap = 
                                                             new ConcurrentHashMap<>();
    private static final Logger LOGGER = LoggerFactory.getLogger(MultiCache.class);

    //每30秒清理一次内存缓存
    private static final int CLEAN_INTERVAL = 30;

    private final RedisTemplate<String, T> redisTemplate;

    @Getter
    @Setter
    private T dbData;

    @PostConstruct
    private void initCleanThread() {
        Thread cleanThread = new Thread(() -> {

            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(CLEAN_INTERVAL * 1000);

                    LOGGER.debug("扫描缓存中过期得垃圾数据....");

                    //JDK8中Map通过EntrySet根据条件进行过滤,清除已过期的key
                    cacheMap.entrySet().removeIf(entry -> Optional.ofNullable(entry.getValue())
                            .map(SoftReference::get)
                            .map(CacheObject::isExpired)
                            //没有value值的对象视为过期(有缓存穿透风险,建议配置为false)
                            .orElse(true));

                } catch (InterruptedException e) {
                    //提示While循环当前线程已经被打断,可退出循环
                    Thread.currentThread().interrupt();
                }
            }

        });
        //设置为守护线程
        cleanThread.setDaemon(true);
        cleanThread.start();
    }

        接下来就是调用Get方法获取缓存了。

public T get(String key) {

        T memoryCache = getMemoryCache(key);
        if (memoryCache != null) {
            LOGGER.warn("从内存中获取值:{}", memoryCache);
            return memoryCache;
        }

        T redisCache = getRedisCache(key);
        if (redisCache != null) {
            LOGGER.warn("从Redis中获取值:{}", redisCache);
            return redisCache;
        }

        T dataFromDb = getDataFromDb(key);
        LOGGER.warn("从数据库中获取值:{}", dataFromDb);
        return dataFromDb;
    }

        首先获取的是内存缓存。如果有内存缓存的话,就直接返回。没有的话就继续往下面走。

   private T getMemoryCache(String key) {
        if (cacheMap.containsKey(key)) {
            SoftReference<CacheObject<T>> cacheFromMemory = cacheMap.get(key);
            if (cacheFromMemory != null && cacheFromMemory.get() != null) {
                return cacheFromMemory.get().getData();
            }
        }
        return null;
    }

        没有内存缓存则向Redis请求数据,并将分布式缓存写入内存缓存中。

   private T getRedisCache(String key) {
        T redisCache = redisTemplate.opsForValue().get(key);

        if (redisCache != null) {
            this.putIntoMemory(key, redisCache);
        }
        return redisCache;
    }

        如果分布式Redis缓存也没有数据,那么只能向数据库请求数据,并同时将DB查询结果写入内存缓存中与Redis缓存中。

    private T getDataFromDb(String key) {
        T dataFromDb = getDbData();
        if (dataFromDb != null) {
            this.putIntoMemory(key, dataFromDb);
            this.putIntoRedis(key, dataFromDb);
        }
        return dataFromDb;
    }

        剩余私有代码

    private void putIntoMemory(String key, T value) {
        CacheObject<T> cacheObject = CacheObject.of(value);
        cacheMap.put(key, new SoftReference<>(cacheObject));
    }

    private void putIntoRedis(String key, T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    private void set(String key, T data) {
        this.putIntoMemory(key, data);
        this.putIntoRedis(key, data);
    }

        最后的Controller测试用例

@RestController
@RequestMapping(value = "/pc/api/v1/cache")
public class UserCacheController ....

    @Autowired
    private MultiCache<UserDO> userCache;

    @GetMapping(value = "/getUser")
    public UserDO getUser(String id) {
        LOGGER.info("paramType is {}", id);

        //模拟数据库中查询到的数据
        userCache.setDbData(new UserDO().setUsername("数据库"));

        return userCache.get(id);
    }

阅读更多 

        跟着大宇学Redis--------目录帖

posted @ 2022-07-17 12:13  小大宇  阅读(277)  评论(0编辑  收藏  举报