【夯实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);
}