Skip to content

Commit

Permalink
LRU and LFU caches implemented. #592
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikita committed Aug 31, 2016
1 parent 5e72484 commit 8d5fba1
Show file tree
Hide file tree
Showing 8 changed files with 1,008 additions and 0 deletions.
524 changes: 524 additions & 0 deletions redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions redisson/src/main/java/org/redisson/misc/Cache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright 2016 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.misc;

import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
*
* @author Nikita Koksharov
*
* @param <K>
* @param <V>
*/
public interface Cache<K, V> extends Map<K, V> {

V put(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit);

}
134 changes: 134 additions & 0 deletions redisson/src/main/java/org/redisson/misc/LFUCacheMap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Copyright 2016 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.misc;

import java.util.Map;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
* LFU (least frequently used) cache.
*
* @author Nikita Koksharov
*
* @param <K>
* @param <V>
*/
public class LFUCacheMap<K, V> extends AbstractCacheMap<K, V> {

public static class MapKey implements Comparable<MapKey> {

private Long accessCount;
private CachedValue cachedValue;

public MapKey(Long accessCount, CachedValue cachedValue) {
super();
this.accessCount = accessCount;
this.cachedValue = cachedValue;
}

@Override
public int compareTo(MapKey o) {
int compare = accessCount.compareTo(o.accessCount);
if (compare == 0) {
if (!cachedValue.equals(o.cachedValue)) {
return ((Integer)cachedValue.hashCode()).compareTo(o.cachedValue.hashCode());
}
return 0;
}
return compare;
}

@Override
public String toString() {
return "MapKey [accessCount=" + accessCount + "]";
}

}

private final ConcurrentNavigableMap<MapKey, CachedValue> accessMap = new ConcurrentSkipListMap<MapKey, CachedValue>();

public LFUCacheMap(int size, long timeToLiveInMillis, long maxIdleInMillis) {
super(size, timeToLiveInMillis, maxIdleInMillis);
}

@Override
protected void onValueCreate(CachedValue value) {
MapKey key = toKey(value);
accessMap.put(key, value);
}

@Override
protected void onValueRead(CachedValue value) {
addAccessCount(value, 1);
}

private MapKey toKey(CachedValue value) {
return new MapKey(value.getAccessCount(), value);
}

@Override
protected void onValueRemove(CachedValue value) {
MapKey key = toKey(value);
if (accessMap.remove(key) == null) {
throw new IllegalStateException();
}
}

private void addAccessCount(CachedValue value, long count) {
synchronized (value) {
if (count < 0 && value.getAccessCount() == 0) {
return;
}

MapKey key = toKey(value);
if (accessMap.remove(key) == null) {
throw new IllegalStateException();
}

if (count < 0) {
count = -Math.min(value.getAccessCount(), -count);
}
value.addAccessCount(count);

key = toKey(value);
accessMap.put(key, value);
}
}

@Override
protected void onMapFull() {
Map.Entry<MapKey, CachedValue> entry = accessMap.pollFirstEntry();
map.remove(entry.getValue().getKey(), entry.getValue());

if (entry.getValue().getAccessCount() == 0) {
return;
}

// TODO optimize
// decrease all values
for (CachedValue value : accessMap.values()) {
addAccessCount(value, -entry.getValue().getAccessCount());
}
}

@Override
public void clear() {
accessMap.clear();
super.clear();
}

}
68 changes: 68 additions & 0 deletions redisson/src/main/java/org/redisson/misc/LRUCacheMap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Copyright 2016 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.misc;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
* LRU (least recently used) cache.
*
* @author Nikita Koksharov
*
* @param <K>
* @param <V>
*/
public class LRUCacheMap<K, V> extends AbstractCacheMap<K, V> {

private final Queue<CachedValue> queue = new ConcurrentLinkedQueue<CachedValue>();

public LRUCacheMap(int size, long timeToLiveInMillis, long maxIdleInMillis) {
super(size, timeToLiveInMillis, maxIdleInMillis);
}

@Override
protected void onValueCreate(CachedValue value) {
queue.add(value);
}

@Override
protected void onValueRemove(CachedValue value) {
queue.remove(value);
}

@Override
protected void onValueRead(CachedValue value) {
// move value to tail of queue
synchronized (value) {
queue.remove(value);
queue.add(value);
}
}

@Override
protected void onMapFull() {
CachedValue value = queue.poll();
map.remove(value.getKey(), value);
}

@Override
public void clear() {
queue.clear();
super.clear();
}

}
48 changes: 48 additions & 0 deletions redisson/src/main/java/org/redisson/misc/NoneCacheMap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright 2016 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.misc;

import java.util.concurrent.TimeUnit;

/**
*
* @author Nikita Koksharov
*
* @param <K>
* @param <V>
*/
public class NoneCacheMap<K, V> extends AbstractCacheMap<K, V> {

public NoneCacheMap(long timeToLiveInMillis, long maxIdleInMillis) {
super(0, timeToLiveInMillis, maxIdleInMillis);
}

@Override
public V put(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit) {
CachedValue entry = new CachedValue(key, value, ttlUnit.toMillis(ttl), maxIdleUnit.toMillis(maxIdleTime));
onValueCreate(entry);
CachedValue prevCachedValue = map.put(key, entry);
if (prevCachedValue != null && !prevCachedValue.isExpired()) {
return (V) prevCachedValue.getValue();
}
return null;
}

@Override
protected void onMapFull() {
}

}
78 changes: 78 additions & 0 deletions redisson/src/test/java/org/redisson/misc/LFUCacheMapTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.redisson.misc;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.concurrent.TimeUnit;

import org.junit.Test;

public class LFUCacheMapTest {

@Test
public void testMaxIdleTimeEviction() throws InterruptedException {
Cache<Integer, Integer> map = new LFUCacheMap<Integer, Integer>(2, 0, 0);
map.put(1, 0, 0, TimeUnit.MILLISECONDS, 400, TimeUnit.MILLISECONDS);
assertThat(map.get(1)).isEqualTo(0);
Thread.sleep(200);
assertThat(map.get(1)).isEqualTo(0);
Thread.sleep(200);
assertThat(map.get(1)).isEqualTo(0);
Thread.sleep(200);
assertThat(map.get(1)).isEqualTo(0);
Thread.sleep(410);
assertThat(map.keySet()).isEmpty();
}

@Test
public void testTTLEviction() throws InterruptedException {
Cache<Integer, Integer> map = new LFUCacheMap<Integer, Integer>(2, 0, 0);
map.put(1, 0, 500, TimeUnit.MILLISECONDS, 0, TimeUnit.MILLISECONDS);
assertThat(map.get(1)).isEqualTo(0);
Thread.sleep(100);
assertThat(map.get(1)).isEqualTo(0);
assertThat(map.keySet()).containsOnly(1);
Thread.sleep(500);
assertThat(map.keySet()).isEmpty();
}

@Test
public void testSizeLFUEviction() throws InterruptedException {
Cache<Integer, Integer> map = new LFUCacheMap<Integer, Integer>(3, 0, 0);

map.put(1, 0);
map.put(2, 0);
map.put(6, 0);

map.get(1);
map.put(3, 0);

assertThat(map.keySet()).containsOnly(3, 1, 6);

map.get(1);
map.put(4, 0);

assertThat(map.keySet()).contains(4, 1).hasSize(3);
}

@Test
public void testSizeEviction() throws InterruptedException {
Cache<Integer, Integer> map = new LFUCacheMap<Integer, Integer>(2, 0, 0);
map.put(1, 0);
map.put(2, 0);

assertThat(map.keySet()).containsOnly(1, 2);

map.put(3, 0);

assertThat(map.keySet()).contains(3).hasSize(2);

map.put(4, 0);

assertThat(map.keySet()).contains(4).hasSize(2);

map.put(5, 0);

assertThat(map.keySet()).contains(5).hasSize(2);
}

}

0 comments on commit 8d5fba1

Please sign in to comment.