移除Redis依赖并改造为内存缓存

This commit is contained in:
wkc
2026-04-15 10:53:27 +08:00
parent 3d4b9a6b29
commit 36f3c32a48
22 changed files with 1864 additions and 609 deletions

View File

@@ -89,30 +89,24 @@
<artifactId>jaxb-api</artifactId>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
<artifactId>yauaa</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
<artifactId>yauaa</artifactId>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
</dependencies>
</project>
<!-- servlet包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,29 @@
package com.ruoyi.common.core.cache;
class InMemoryCacheEntry
{
private final Object value;
private final Long expireAtMillis;
InMemoryCacheEntry(Object value, Long expireAtMillis)
{
this.value = value;
this.expireAtMillis = expireAtMillis;
}
Object getValue()
{
return value;
}
Long getExpireAtMillis()
{
return expireAtMillis;
}
boolean isExpired(long now)
{
return expireAtMillis != null && expireAtMillis.longValue() <= now;
}
}

View File

@@ -0,0 +1,65 @@
package com.ruoyi.common.core.cache;
public class InMemoryCacheStats
{
private final String cacheType;
private final String mode;
private final long keySize;
private final long hitCount;
private final long missCount;
private final long expiredCount;
private final long writeCount;
public InMemoryCacheStats(String cacheType, String mode, long keySize, long hitCount, long missCount,
long expiredCount, long writeCount)
{
this.cacheType = cacheType;
this.mode = mode;
this.keySize = keySize;
this.hitCount = hitCount;
this.missCount = missCount;
this.expiredCount = expiredCount;
this.writeCount = writeCount;
}
public String getCacheType()
{
return cacheType;
}
public String getMode()
{
return mode;
}
public long getKeySize()
{
return keySize;
}
public long getHitCount()
{
return hitCount;
}
public long getMissCount()
{
return missCount;
}
public long getExpiredCount()
{
return expiredCount;
}
public long getWriteCount()
{
return writeCount;
}
}

View File

@@ -0,0 +1,290 @@
package com.ruoyi.common.core.cache;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;
@Component
public class InMemoryCacheStore
{
private final ConcurrentHashMap<String, InMemoryCacheEntry> entries = new ConcurrentHashMap<String, InMemoryCacheEntry>();
private final AtomicLong hitCount = new AtomicLong();
private final AtomicLong missCount = new AtomicLong();
private final AtomicLong expiredCount = new AtomicLong();
private final AtomicLong writeCount = new AtomicLong();
public void set(String key, Object value)
{
putEntry(key, new InMemoryCacheEntry(value, null));
}
public void set(String key, Object value, long timeout, TimeUnit unit)
{
Objects.requireNonNull(unit, "TimeUnit must not be null");
long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
putEntry(key, new InMemoryCacheEntry(value, Long.valueOf(expireAtMillis)));
}
@SuppressWarnings("unchecked")
public <T> T get(String key)
{
InMemoryCacheEntry entry = readEntry(key);
return entry == null ? null : (T) entry.getValue();
}
public boolean hasKey(String key)
{
return readEntry(key) != null;
}
public boolean delete(String key)
{
return entries.remove(key) != null;
}
public boolean delete(Collection<String> keys)
{
boolean deleted = false;
if (keys == null)
{
return false;
}
for (String key : keys)
{
deleted = delete(key) || deleted;
}
return deleted;
}
public Set<String> keys(String pattern)
{
purgeExpiredEntries();
Set<String> matchedKeys = new TreeSet<String>();
for (Map.Entry<String, InMemoryCacheEntry> entry : entries.entrySet())
{
if (matches(pattern, entry.getKey()))
{
matchedKeys.add(entry.getKey());
}
}
return matchedKeys;
}
public boolean expire(String key, long timeout, TimeUnit unit)
{
Objects.requireNonNull(unit, "TimeUnit must not be null");
long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
return entries.computeIfPresent(key, (cacheKey, entry) -> entry.isExpired(System.currentTimeMillis())
? null
: new InMemoryCacheEntry(entry.getValue(), Long.valueOf(expireAtMillis))) != null;
}
public long getExpire(String key)
{
return getExpire(key, TimeUnit.SECONDS);
}
public long getExpire(String key, TimeUnit unit)
{
Objects.requireNonNull(unit, "TimeUnit must not be null");
InMemoryCacheEntry entry = readEntry(key);
if (entry == null)
{
return -2L;
}
if (entry.getExpireAtMillis() == null)
{
return -1L;
}
long remainingMillis = Math.max(0L, entry.getExpireAtMillis().longValue() - System.currentTimeMillis());
long unitMillis = Math.max(1L, unit.toMillis(1L));
return (remainingMillis + unitMillis - 1L) / unitMillis;
}
public long increment(String key, long timeout, TimeUnit unit)
{
Objects.requireNonNull(unit, "TimeUnit must not be null");
AtomicLong result = new AtomicLong();
entries.compute(key, (cacheKey, currentEntry) -> {
long now = System.currentTimeMillis();
boolean missingOrExpired = currentEntry == null || currentEntry.isExpired(now);
long nextValue = missingOrExpired ? 1L : toLong(currentEntry.getValue()) + 1L;
Long expireAtMillis = missingOrExpired || currentEntry.getExpireAtMillis() == null
? Long.valueOf(now + Math.max(0L, unit.toMillis(timeout)))
: currentEntry.getExpireAtMillis();
if (missingOrExpired && currentEntry != null)
{
expiredCount.incrementAndGet();
}
result.set(nextValue);
return new InMemoryCacheEntry(Long.valueOf(nextValue), expireAtMillis);
});
writeCount.incrementAndGet();
return result.get();
}
public void clear()
{
entries.clear();
}
public InMemoryCacheStats snapshot()
{
purgeExpiredEntries();
return new InMemoryCacheStats("IN_MEMORY", "single-instance", entries.size(), hitCount.get(), missCount.get(),
expiredCount.get(), writeCount.get());
}
@SuppressWarnings("unchecked")
public <T> Map<String, T> getMap(String key)
{
Map<String, T> value = get(key);
return value == null ? null : new HashMap<String, T>(value);
}
public <T> void putMap(String key, Map<String, T> dataMap)
{
set(key, new HashMap<String, T>(dataMap));
}
public <T> void putMapValue(String key, String mapKey, T value)
{
long ttl = getExpire(key, TimeUnit.MILLISECONDS);
Map<String, T> current = getMap(key);
if (current == null)
{
current = new HashMap<String, T>();
}
current.put(mapKey, value);
setWithOptionalTtl(key, current, ttl);
}
public boolean deleteMapValue(String key, String mapKey)
{
long ttl = getExpire(key, TimeUnit.MILLISECONDS);
Map<String, Object> current = getMap(key);
if (current == null || !current.containsKey(mapKey))
{
return false;
}
current.remove(mapKey);
setWithOptionalTtl(key, current, ttl);
return true;
}
@SuppressWarnings("unchecked")
public <T> Set<T> getSet(String key)
{
Set<T> value = get(key);
return value == null ? null : new HashSet<T>(value);
}
public <T> void putSet(String key, Set<T> dataSet)
{
set(key, new HashSet<T>(dataSet));
}
@SuppressWarnings("unchecked")
public <T> List<T> getList(String key)
{
List<T> value = get(key);
return value == null ? null : new ArrayList<T>(value);
}
public <T> void putList(String key, List<T> dataList)
{
set(key, new ArrayList<T>(dataList));
}
private void putEntry(String key, InMemoryCacheEntry entry)
{
entries.put(key, entry);
writeCount.incrementAndGet();
}
private InMemoryCacheEntry readEntry(String key)
{
InMemoryCacheEntry entry = entries.get(key);
if (entry == null)
{
missCount.incrementAndGet();
return null;
}
if (entry.isExpired(System.currentTimeMillis()))
{
removeExpiredEntry(key, entry);
missCount.incrementAndGet();
return null;
}
hitCount.incrementAndGet();
return entry;
}
private void purgeExpiredEntries()
{
long now = System.currentTimeMillis();
for (Map.Entry<String, InMemoryCacheEntry> entry : entries.entrySet())
{
if (entry.getValue().isExpired(now))
{
removeExpiredEntry(entry.getKey(), entry.getValue());
}
}
}
private void removeExpiredEntry(String key, InMemoryCacheEntry expectedEntry)
{
if (entries.remove(key, expectedEntry))
{
expiredCount.incrementAndGet();
}
}
private boolean matches(String pattern, String key)
{
if ("*".equals(pattern))
{
return true;
}
if (pattern.endsWith("*"))
{
return key.startsWith(pattern.substring(0, pattern.length() - 1));
}
return key.equals(pattern);
}
private long toLong(Object value)
{
if (value instanceof Number)
{
return ((Number) value).longValue();
}
return Long.parseLong(String.valueOf(value));
}
private void setWithOptionalTtl(String key, Object value, long ttlMillis)
{
if (ttlMillis > 0L)
{
set(key, value, ttlMillis, TimeUnit.MILLISECONDS);
}
else
{
set(key, value);
}
}
}

View File

@@ -1,40 +1,42 @@
package com.ruoyi.common.core.redis;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
/**
* spring redis 工具类
*
* @author ruoyi
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象Integer、String、实体类等
package com.ruoyi.common.core.redis;
import com.ruoyi.common.core.cache.InMemoryCacheStats;
import com.ruoyi.common.core.cache.InMemoryCacheStore;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Component;
/**
* 本地缓存门面,保留原有 RedisCache 业务入口。
*
* @author ruoyi
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
private final InMemoryCacheStore cacheStore;
public RedisCache(InMemoryCacheStore cacheStore)
{
this.cacheStore = cacheStore;
}
/**
* 缓存基本的对象Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
*/
public <T> void setCacheObject(final String key, final T value)
{
cacheStore.set(key, value);
}
/**
* 缓存基本的对象Integer、String、实体类等
@@ -44,10 +46,10 @@ public class RedisCache
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
cacheStore.set(key, value, timeout.longValue(), timeUnit);
}
/**
* 设置有效时间
@@ -69,10 +71,10 @@ public class RedisCache
* @param unit 时间单位
* @return true=设置成功false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return cacheStore.expire(key, timeout, unit);
}
/**
* 获取有效时间
@@ -80,10 +82,10 @@ public class RedisCache
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}
public long getExpire(final String key)
{
return cacheStore.getExpire(key);
}
/**
* 判断 key是否存在
@@ -91,10 +93,10 @@ public class RedisCache
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}
public Boolean hasKey(String key)
{
return cacheStore.hasKey(key);
}
/**
* 获得缓存的基本对象。
@@ -102,21 +104,20 @@ public class RedisCache
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
public <T> T getCacheObject(final String key)
{
return cacheStore.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
public boolean deleteObject(final String key)
{
return cacheStore.delete(key);
}
/**
* 删除集合对象
@@ -124,10 +125,19 @@ public class RedisCache
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection)
{
return redisTemplate.delete(collection) > 0;
}
public boolean deleteObject(final Collection collection)
{
if (collection == null || collection.isEmpty())
{
return false;
}
List<String> keys = new ArrayList<String>(collection.size());
for (Object item : collection)
{
keys.add(String.valueOf(item));
}
return cacheStore.delete(keys);
}
/**
* 缓存List数据
@@ -136,11 +146,11 @@ public class RedisCache
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
public <T> long setCacheList(final String key, final List<T> dataList)
{
cacheStore.putList(key, dataList);
return dataList == null ? 0 : dataList.size();
}
/**
* 获得缓存的list对象
@@ -148,10 +158,10 @@ public class RedisCache
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
public <T> List<T> getCacheList(final String key)
{
return cacheStore.getList(key);
}
/**
* 缓存Set
@@ -160,16 +170,11 @@ public class RedisCache
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
public <T> long setCacheSet(final String key, final Set<T> dataSet)
{
cacheStore.putSet(key, dataSet);
return dataSet == null ? 0 : dataSet.size();
}
/**
* 获得缓存的set
@@ -177,10 +182,10 @@ public class RedisCache
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
public <T> Set<T> getCacheSet(final String key)
{
return cacheStore.getSet(key);
}
/**
* 缓存Map
@@ -188,12 +193,13 @@ public class RedisCache
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null)
{
cacheStore.putMap(key, dataMap);
}
}
/**
* 获得缓存的Map
@@ -201,10 +207,10 @@ public class RedisCache
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
public <T> Map<String, T> getCacheMap(final String key)
{
return cacheStore.getMap(key);
}
/**
* 往Hash中存入数据
@@ -213,10 +219,10 @@ public class RedisCache
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
cacheStore.putMapValue(key, hKey, value);
}
/**
* 获取Hash中的数据
@@ -225,11 +231,11 @@ public class RedisCache
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
public <T> T getCacheMapValue(final String key, final String hKey)
{
Map<String, T> map = cacheStore.getMap(key);
return map == null ? null : map.get(hKey);
}
/**
* 获取多个Hash中的数据
@@ -238,10 +244,20 @@ public class RedisCache
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
Map<String, T> map = cacheStore.getMap(key);
if (map == null || hKeys == null || hKeys.isEmpty())
{
return Collections.emptyList();
}
List<T> values = new ArrayList<T>(hKeys.size());
for (Object hKey : hKeys)
{
values.add(map.get(String.valueOf(hKey)));
}
return values;
}
/**
* 删除Hash中的某条数据
@@ -250,10 +266,10 @@ public class RedisCache
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return cacheStore.deleteMapValue(key, hKey);
}
/**
* 获得缓存的基本对象列表
@@ -261,8 +277,23 @@ public class RedisCache
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
public Collection<String> keys(final String pattern)
{
return cacheStore.keys(pattern);
}
public long increment(String key, long timeout, TimeUnit unit)
{
return cacheStore.increment(key, timeout, unit);
}
public InMemoryCacheStats getCacheStats()
{
return cacheStore.snapshot();
}
public void clear()
{
cacheStore.clear();
}
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.common.core.cache;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
class InMemoryCacheStoreTest
{
@Test
void shouldExpireEntryAfterTtl() throws Exception
{
InMemoryCacheStore store = new InMemoryCacheStore();
store.set("captcha_codes:1", "1234", 20L, TimeUnit.MILLISECONDS);
Thread.sleep(40L);
assertNull(store.get("captcha_codes:1"));
}
@Test
void shouldReturnPrefixKeysInSortedOrder()
{
InMemoryCacheStore store = new InMemoryCacheStore();
store.set("login_tokens:a", "A");
store.set("login_tokens:b", "B");
store.set("sys_dict:x", "X");
Set<String> expected = new HashSet<String>();
expected.add("login_tokens:a");
expected.add("login_tokens:b");
assertEquals(expected, store.keys("login_tokens:*"));
}
@Test
void shouldTrackHitsMissesWritesAndExpiredEntries() throws Exception
{
InMemoryCacheStore store = new InMemoryCacheStore();
store.set("captcha_codes:2", "5678", 20L, TimeUnit.MILLISECONDS);
assertTrue(store.hasKey("captcha_codes:2"));
assertEquals("5678", store.get("captcha_codes:2"));
Thread.sleep(40L);
assertNull(store.get("captcha_codes:2"));
assertFalse(store.hasKey("captcha_codes:2"));
InMemoryCacheStats stats = store.snapshot();
assertEquals("IN_MEMORY", stats.getCacheType());
assertEquals("single-instance", stats.getMode());
assertEquals(0, stats.getKeySize());
assertEquals(2L, stats.getHitCount());
assertEquals(2L, stats.getMissCount());
assertEquals(1L, stats.getExpiredCount());
assertEquals(1L, stats.getWriteCount());
}
}

View File

@@ -0,0 +1,56 @@
package com.ruoyi.common.core.redis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.ruoyi.common.core.cache.InMemoryCacheStore;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
class RedisCacheTest
{
@Test
void shouldSupportSetGetDeleteAndExpireThroughRedisCacheFacade()
{
RedisCache cache = new RedisCache(new InMemoryCacheStore());
cache.setCacheObject("login_tokens:abc", "payload", 1, TimeUnit.SECONDS);
assertEquals("payload", cache.getCacheObject("login_tokens:abc"));
assertTrue(cache.hasKey("login_tokens:abc"));
assertTrue(cache.deleteObject("login_tokens:abc"));
}
@Test
void shouldSupportKeysBulkDeleteAndRemainingTtl()
{
RedisCache cache = new RedisCache(new InMemoryCacheStore());
cache.setCacheObject("login_tokens:a", "A", 5, TimeUnit.SECONDS);
cache.setCacheObject("login_tokens:b", "B", 5, TimeUnit.SECONDS);
cache.setCacheObject("sys_dict:x", "X");
assertNotNull(cache.getExpire("login_tokens:a"));
assertTrue(cache.getExpire("login_tokens:a") > 0L);
assertEquals(2, cache.keys("login_tokens:*").size());
Collection<String> keys = new ArrayList<String>();
keys.add("login_tokens:a");
keys.add("login_tokens:b");
assertTrue(cache.deleteObject(keys));
assertFalse(cache.hasKey("login_tokens:a"));
}
@Test
void shouldIncrementCounterWithinTtlWindow() throws Exception
{
RedisCache cache = new RedisCache(new InMemoryCacheStore());
assertEquals(1L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
assertEquals(2L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
Thread.sleep(70L);
assertNull(cache.getCacheObject("rate_limit:test"));
assertEquals(1L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
}
}