替代Redis存储Token
对于单体应用来说,完全可以用Caffeine取代Redis。
但Token
是有时效性的,这一点Redis可以很容易做到。如果自定义缓存计时非常麻烦,大部分中间件又没有过期失效,那么Caffeine该如何替代Redis呢?
通过对缓存(而非Redis)功能的分析,可知两点。
只要缓存失效即可,是否
过期
不是目的。删除过期值完全可以用代码实现。
所以可以利用MongoDB + Caffeine的方式替代Redis存储Token
。
引入依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<exclusions>
<exclusion>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
</exclusion>
</exclusions>
</dependency>
增加配置。
## MONGO
spring.data.mongodb.host=172.16.185.135
spring.data.mongodb.port=27017
spring.data.mongodb.database=0
spring.data.mongodb.username=test
spring.data.mongodb.password=123456
定义MongoDB配置类。
/**
* 去除_class字段的配置类
*
* @author 湘王
*/
@Configuration
public class MongoConfigure implements InitializingBean {
@Resource
private MappingMongoConverter mappingConverter;
@Override
public void afterPropertiesSet() throws Exception {
// 去除插入数据库的_class字段
mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
}
}
定义Cache
类(注意time
字段)。
/**
* 缓存Document
*
*/
public class Cache implements Serializable {
private static final long serialVersionUID = 7353685666928500768L;
@Id
private String id;
private String key;
private String value;
private long time;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@Override
public String toString() {
return String.format("{\"id\":\"%s\", \"key\":\"%s\", \"value\":\"%s\", \"time\":%d}", id, key, value, time);
}
}
定义操作Caffeine的方法。
/**
* 缓存Dao
*
*/
@Component
public class CacheDao<T> {
@Resource
private MongoTemplate mongoTemplate;
// expiretime指的是从存储到失效之间的时间间隔,单位毫秒
@Cacheable(value = "test", key = "#key")
public String getObject(final String key, final long expiretime) {
Query query = new Query(Criteria.where("key").is(key));
Cache cache = (Cache) mongoTemplate.findOne(query, Cache.class);
System.out.println("getObject(" + key + ", " + expiretime + ") from mongo");
if (null != cache) {
// -1表示永不过期
if (-1 == expiretime) {
return cache.getValue();
}
// 如果当前时间 - 存储cache时的时间 >= 过期间隔
long currentTtime = System.currentTimeMillis();
if (currentTtime - cache.getTime() >= expiretime * 1000) {
// 删除key,并返回null
removeObject(key);
} else {
return cache.getValue();
}
}
return null;
}
// 保存时,需要增加过期时间,方便同步到Caffeine
@CachePut(value = "test", key = "#key")
public boolean saveObject(final String key, final String value) {
Query query = new Query(Criteria.where("key").is(key));
Update update = new Update();
long time = System.currentTimeMillis();
update.set("key", key);
update.set("value", value);
update.set("time", time);
try {
UpdateResult result = mongoTemplate.upsert(query, update, Cache.class);
if (result.wasAcknowledged()) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@CacheEvict(value = "test", key = "#key")
public boolean removeObject(final String key) {
Query query = new Query(Criteria.where("key").is(key));
try {
DeleteResult result = mongoTemplate.remove(query, Cache.class);
if (result.wasAcknowledged()) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
再定义缓存服务类。
/**
* 缓存Service接口
*
*/
@Service
public class CacheService {
@Autowired
private CacheDao<Cache> cacheDao;
public String getObject(final String key, final long expiretime) {
return cacheDao.getObject(key, expiretime);
}
public boolean saveObject(final String key, final String value) {
return cacheDao.saveObject(key, value);
}
public boolean removeObject(final String key) {
return cacheDao.removeObject(key);
}
}
最后创建控制器类。
/**
* Cache控制器
*
*/
@RestController
public class CacheController {
@Autowired
private CacheService cacheService;
@GetMapping("/cache/save")
public void save(final String key, final String value) {
cacheService.saveObject(key, value);
}
// 获取数据,过期时间为秒(会转换为毫秒)
@GetMapping("/cache/get")
public String get(final String key, final int expiretime) {
String result = cacheService.getObject(key, expiretime);
if (null == result) {
return "expire value";
}
return result;
}
}
先保存KV
,再获取key
,过期时间为3秒。
发现即使过了3秒,还是能获取到保存的数据。
这是因为之前在整合Springboot时,使用的是注解方式,在配置文件中已经写死了Caffeine的过期时间是5分钟,所以当然能读取到。
## CAFFEINE
......
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=300s
......
而且使用注解式的Caffeine,应用一旦启动,是无法动态调整过期时间的,必然与MongoDB时间不同步。
进一步延伸思考:Caffeine是没有持久化功能的,所以当应用重新启动的时候,上一次设置的过期时间会被重置。
因此Caffeine + MongoDB替代Redis其实需要解决一个很关键的问题,那就是:Caffeine和MongoDB的过期时间需要同步,也就是Caffeine的过期时间要能够灵活调整。
所以,放弃注解式Caffeine,使用自定义LoadingCache
。
修改操作Caffeine的方法。
/**
* 缓存Dao
*
*/
@Component
public class CacheDao<T> {
private static LoadingCache<String, String> loadingCache = null;
/**
* 自定义LoadingCache,指定过期时间expiretime
*
*/
private LoadingCache<String, String> initCache(long expiretime) {
return Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(100)
.expireAfterWrite(expiretime, TimeUnit.MILLISECONDS)
.build(key -> {
// 没有数据或过期时返回null
return null;
});
}
/**
* 保存时,需要增加过期时间,方便同步到Caffeine
*
* @param key
* @param value
* @param expiretime
* @return
*/
public boolean saveObject(final String key, final String value, final long expiretime) {
Query query = new Query(Criteria.where("key").is(key));
Update update = new Update();
long time = System.currentTimeMillis();
update.set("key", key);
update.set("value", value);
update.set("time", time);
try {
UpdateResult result = mongoTemplate.upsert(query, update, Cache.class);
if (result.wasAcknowledged()) {
// 同步到Caffeie
loadingCache = initCache(expiretime * 1000);
loadingCache.put(key, value);
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
// expiretime指的是从存储到失效之间的时间间隔,单位毫秒
public String getObject(final String key, final long expiretime) {
String result = null;
// loadingCache不为空说明之前已经同步过了,可以直接读取它的值
if (null != loadingCache) {
result = loadingCache.get(key);
if (null != result) {
// 读取到值时,直接返回,读取不到就去mongodb读取
return result;
}
}
Query query = new Query(Criteria.where("key").is(key));
Cache cache = (Cache) mongoTemplate.findOne(query, Cache.class);
System.out.println("getObject(" + key + ", " + expiretime + ") from mongo");
if (null != cache) {
// -1表示永不过期
if (-1 == expiretime) {
return cache.getValue();
}
// 如果当前时间 - 存储cache时的时间 >= 过期间隔
long currentTtime = System.currentTimeMillis();
if (currentTtime - cache.getTime() >= expiretime * 1000) {
// 删除key,并返回null
removeObject(key);
} else {
/**
* 需要计算出当前时间与过期时间之间的差值,并赋予Caffeine的失效时间
* 计算过程分析:
* 保存时间:00:00
* 当前时间:00:03
* 过期时间:10秒
* 那么第一次读取时需要将剩余的7秒赋给Caffeine
*/
if (null == loadingCache) {// loadingCache==null说明loadingCache需要同步
loadingCache = initCache(expiretime * 1000 - (currentTtime - cache.getTime()));
loadingCache.put(key, cache.getValue());
}
return cache.getValue();
}
}
return null;
}
}
在这个重新定义的类中。
增加了
LoadingCache
。修改了
saveObject()
方法。关键是对
getObject()
的修改。
由于保存时增加了过期时间,所以Service
和Controller
类也要同时修改。
public boolean saveObject(final String key, final String value, final long expiretime) {
return cacheDao.saveObject(key, value, expiretime);
}
@GetMapping("/cache/save")
public void save(final String key, final String value, final long expiretime) {
cacheService.saveObject(key, value, expiretime);
}
启动应用,通过
save()
保存,再通过get()
读取——测试成功。启动应用,通过
get()
读取,读取不到值(因为未设置)——测试成功。启动应用,通过
save()
保存,停止服务并稍后重启(可以在过期时间内重启,也可以在过期时间外重启)。通过
get()
读取,如果是在有效期内,能够读取到值——测试成功。通过
get()
读取,如果超过有效期,就读取不到值了——测试成功。
感谢支持
更多内容,请移步《超级个体》。