实际项目中我打算把用户和组织信息放到缓存中,基于此提出以下几点需求:
1.数据存储在内存中;
2.允许以键值对的方式存储对象类数据并带有过期策略;
3.不限制内存使用,但cache也不能给我报出OutOfMemoryErrormemory异常;
4.cache要自动清理过期对象
5.线程安全
为了满足以上需求,本例子中主要使用了以下技术:
1.ConcurrentHashMap
2.DelayQueue
3.SoftReference
4.Thread
5.java8的Optional及函数式编程
这里提供2中编码实现,先定义一个接口,用于规范和统一方法:
package com.dylan.springboot.helloweb.cache;
/**
* @Description: 自定义缓存接口
* @Author laoxu
* @Date 2019/7/27 13:45
**/
public interface ICache {
void add(String key, Object value, long periodInMillis);
void remove(String key);
Object get(String key);
void clear();
long size();
}
package com.dylan.springboot.helloweb.cache;
/**
* @Description: 带过期时间的缓存对象
* @Author laoxu
* @Date 2019/7/27 14:27
**/
public class CacheObject {
private Object value;
private long expiryTime;
public CacheObject(Object value, long expiryTime) {
this.value = value;
this.expiryTime = expiryTime;
}
public Object getValue() {
return value;
}
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
}
package com.dylan.springboot.helloweb.cache;
import java.lang.ref.SoftReference;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Description: 方案1,缓存实现
* @Author laoxu
* @Date 2019/7/27 13:47
**/
// @Component
public class CacheImpl implements ICache {
private static final int CLEAN_UP_PERIOD_IN_SEC = 5;
private final ConcurrentHashMap> cache = new ConcurrentHashMap<>();
public CacheImpl() {
Thread cleanerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(CLEAN_UP_PERIOD_IN_SEC * 1000);
cache.entrySet().removeIf(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(CacheObject::isExpired).orElse(false));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
cleanerThread.setDaemon(true);
cleanerThread.start();
}
@Override
public void add(String key, Object value, long periodInMillis) {
if (key == null) {
return;
}
if (value == null) {
cache.remove(key);
} else {
long expiryTime = System.currentTimeMillis() + periodInMillis;
cache.put(key, new SoftReference<>(new CacheObject(value, expiryTime)));
}
}
@Override
public void remove(String key) {
cache.remove(key);
}
@Override
public Object get(String key) {
return Optional.ofNullable(cache.get(key)).map(SoftReference::get).filter(cacheObject -> !cacheObject.isExpired()).map(CacheObject::getValue).orElse(null);
}
@Override
public void clear() {
cache.clear();
}
@Override
public long size() {
return cache.entrySet().stream().filter(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(cacheObject -> !cacheObject.isExpired()).orElse(false)).count();
}
}
此方案有2大缺陷:
1.如果map中存储了大量的对象那么扫描这些对象并清理需要花不少时间;
2.此处的size()方法将花费O(n)时间因为它必须过滤掉过期对象。
package com.dylan.springboot.helloweb.cache;
import java.lang.ref.SoftReference;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @Description: 带key和过期时间的缓存对象
* @Author laoxu
* @Date 2019/7/27 15:28
**/
public class DelayedCacheObject implements Delayed {
private final String key;
private final SoftReference
package com.dylan.springboot.helloweb.cache;
import org.springframework.stereotype.Component;
import java.lang.ref.SoftReference;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
/**
* @Description: 方案2,缓存实现
* @Author laoxu
* @Date 2019/7/27 15:24
**/
@Component
public class InMemoryCacheWithDelayQueue implements ICache {
private final ConcurrentHashMap> cache = new ConcurrentHashMap<>();
private final DelayQueue cleaningUpQueue = new DelayQueue<>();
public InMemoryCacheWithDelayQueue() {
Thread cleanerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
DelayedCacheObject delayedCacheObject = cleaningUpQueue.take();
cache.remove(delayedCacheObject.getKey(), delayedCacheObject.getReference());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
cleanerThread.setDaemon(true);
cleanerThread.start();
}
@Override
public void add(String key, Object value, long periodInMillis) {
if (key == null) {
return;
}
if (value == null) {
cache.remove(key);
} else {
long expiryTime = System.currentTimeMillis() + periodInMillis;
SoftReference reference = new SoftReference<>(value);
cache.put(key, reference);
cleaningUpQueue.put(new DelayedCacheObject(key, reference, expiryTime));
}
}
@Override
public void remove(String key) {
cache.remove(key);
}
@Override
public Object get(String key) {
return Optional.ofNullable(cache.get(key)).map(SoftReference::get).orElse(null);
}
@Override
public void clear() {
cache.clear();
}
@Override
public long size() {
return cache.size();
}
}
为了方便测试,我在spring boot中添加了controller:
package com.dylan.springboot.helloweb.controller;
import com.dylan.springboot.helloweb.cache.ICache;
import com.dylan.springboot.helloweb.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @Description: 测试缓存
* @Author laoxu
* @Date 2019/7/27 14:35
**/
@RestController
@RequestMapping("/cache")
public class CacheController {
@Autowired
private ICache cache;
@GetMapping("/size")
public Long getSize(){
return cache.size();
}
@PostMapping("/add")
public String add(@RequestBody Student student){
cache.add(student.getName(),student,60000);
return "success";
}
@GetMapping("/get")
public Student get(String name){
Student student = (Student) cache.get(name);
return student;
}
}
相关测试截图:
添加:
查询
1分钟内查看大小
1分钟后查看大小