├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── spring2go │ ├── bigcache │ ├── BigCache.java │ ├── BigCacheStats.java │ ├── CacheConfig.java │ ├── CacheValueWrapper.java │ ├── ICache.java │ ├── lock │ │ └── StripedReadWriteLock.java │ ├── storage │ │ ├── AlignOffHeapStorage.java │ │ ├── FileChannelStorage.java │ │ ├── IStorage.java │ │ ├── IStorageBlock.java │ │ ├── MemoryMappedStorage.java │ │ ├── OffHeapStorage.java │ │ ├── Pointer.java │ │ ├── StorageBlock.java │ │ └── StorageManager.java │ └── utils │ │ └── FileUtil.java │ ├── lrucache │ ├── v1 │ │ └── LruCacheV1.java │ ├── v2 │ │ └── LruCacheV2.java │ └── v3 │ │ └── LruCacheV3.java │ └── okcache │ ├── AbstractDeque.java │ ├── BehindStore.java │ ├── Cache.java │ ├── CacheBuilder.java │ ├── CacheEntry.java │ ├── CacheEntryHelper.java │ ├── CacheStats.java │ ├── CacheStatsCounter.java │ ├── CacheStore.java │ ├── DummyCacheStore.java │ ├── ExpirationPolicy.java │ ├── ICache.java │ ├── JavaSerializer.java │ ├── Serializer.java │ ├── TimeHelper.java │ ├── impl │ ├── CacheImpl.java │ ├── CacheSegment.java │ ├── HashCacheEntry.java │ ├── SegmentAccessQueue.java │ └── SegmentStatsCounter.java │ └── store │ └── BigCacheStore.java └── test └── java └── com └── spring2go ├── bigcache ├── BigCacheCleanerTest.java ├── BigCacheLimitTest.java ├── BigCachePerfTestA.java ├── BigCachePerfTestB.java ├── BigCacheReadWriteStressTest.java ├── BigCacheStressTest.java ├── BigCacheTest.java ├── BigCacheUnitTest.java ├── storage │ ├── FileChannelStorageTest.java │ ├── StorageBlockTest.java │ ├── StorageManagerTest.java │ └── StorageUnitTest.java └── utils │ ├── TestSample.java │ └── TestUtil.java ├── lrucache ├── v1 │ └── LruCacheV1Test.java ├── v2 │ ├── LruCacheV2PerfTest.java │ └── LruCacheV2Test.java └── v3 │ ├── LruCacheV3PerfTest.java │ └── LruCacheV3Test.java └── okcache ├── CacheImplTest.java ├── CachePerfGetsTest.java ├── CachePerfTest.java ├── CacheWithEvictStoreTest.java └── JavaSerializationTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | 3 | ### IntelliJ IDEA ### 4 | .idea 5 | *.iws 6 | *.iml 7 | *.ipr 8 | 9 | # Eclipse IDE metadata 10 | .classpath 11 | .project 12 | .settings/ 13 | 14 | # Mac files 15 | .DS_Store 16 | /.idea/ 17 | dltestDB:/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 波波微课 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OkCache 2 | 一个高性能二级缓存实现, 内存LRU缓存 + 磁盘文件持久化缓存。 3 | 4 | * 支持过期(Expiration)清除; 5 | * 支持LRU ~ 如果超过内存缓存容量,最近不常使用的项将被剔除(Eviction); 6 | * 支持剔除(Eviction)到二级可持久化缓存(BigCache); 7 | * 支持回写(Write Behind)到后端持久化存储,例如DB。 8 | * BigCache的Key常驻内存,Value可持久化。 9 | * BigCache支持纯磁盘文件,内存映射+磁盘文件,和堆外内存+磁盘文件三种模式。 10 | 11 | ## 注意 12 | 13 | 1. 运行BigCache单元测试前,请将TestUtil.TEST_BASE_DIR修改为本地测试目录: 14 | 2. 回写到MySQL的代码未开源,需要的自己实现`BehindStorage`接口即可。 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.spring2go 7 | okcache 8 | 1.0-SNAPSHOT 9 | 10 | 11 | 1.8 12 | 1.8 13 | 14 | 15 | 16 | 17 | junit 18 | junit 19 | 4.13 20 | test 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/BigCacheStats.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | /** 4 | * Created on Jul, 2020 by @author bobo 5 | */ 6 | public class BigCacheStats { 7 | private final long cacheHit; 8 | private final long cacheMiss; 9 | 10 | private final long cacheGet; 11 | private final long cachePut; 12 | private final long cacheDelete; 13 | 14 | private final long cacheExpire; 15 | private final long cacheMove; 16 | private final long cacheTotalEntries; 17 | 18 | private final long storageUsed; 19 | private final long storageDirty; 20 | private final long storageCapacity; 21 | private final long storageUsedBlocks; 22 | private final long storageFreeBlocks; 23 | private final long storageTotalBlocks; 24 | 25 | public BigCacheStats(long cacheHit, long cacheMiss, long cacheGet, 26 | long cachePut, long cacheDelete, long cacheExpire, 27 | long cacheMove, long cacheTotalEntries, long storageUsed, 28 | long storageDirty, long storageCapacity, 29 | long storageUsedBlocks, long storageFreeBlocks, long storageTotalBlocks) { 30 | this.cacheHit = cacheHit; 31 | this.cacheMiss = cacheMiss; 32 | 33 | this.cacheGet = cacheGet; 34 | this.cachePut = cachePut; 35 | this.cacheDelete = cacheDelete; 36 | 37 | this.cacheExpire = cacheExpire; 38 | this.cacheMove = cacheMove; 39 | 40 | this.cacheTotalEntries = cacheTotalEntries; 41 | 42 | this.storageUsed = storageUsed; 43 | this.storageDirty = storageDirty; 44 | this.storageCapacity = storageCapacity; 45 | 46 | this.storageUsedBlocks = storageUsedBlocks; 47 | this.storageFreeBlocks = storageFreeBlocks; 48 | this.storageTotalBlocks = storageTotalBlocks; 49 | } 50 | 51 | public BigCacheStats() { 52 | this(0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L); 53 | } 54 | 55 | public BigCacheStats getDeltaStats(BigCacheStats previousStats) { 56 | if (previousStats == null) { 57 | return this; 58 | } 59 | return new BigCacheStats( 60 | /*$(current value - previous value): the delta between two adjacent stats*/ 61 | this.cacheHit - previousStats.cacheHit, 62 | this.cacheMiss - previousStats.cacheMiss, 63 | this.cacheGet - previousStats.cacheGet, 64 | this.cachePut - previousStats.cachePut, 65 | this.cacheDelete - previousStats.cacheDelete, 66 | this.cacheExpire - previousStats.cacheExpire, 67 | this.cacheMove - previousStats.cacheMove, 68 | 69 | /*$(current value): latest value which is more meaningful*/ 70 | this.cacheTotalEntries, 71 | this.storageUsed, 72 | this.storageDirty, 73 | this.storageCapacity, 74 | this.storageUsedBlocks, 75 | this.storageFreeBlocks, 76 | this.storageTotalBlocks 77 | ); 78 | } 79 | 80 | public long getCacheHit() { 81 | return cacheHit; 82 | } 83 | 84 | public long getCacheMiss() { 85 | return cacheMiss; 86 | } 87 | 88 | public long getCacheGet() { 89 | return cacheGet; 90 | } 91 | 92 | public long getCachePut() { 93 | return cachePut; 94 | } 95 | 96 | public long getCacheDelete() { 97 | return cacheDelete; 98 | } 99 | 100 | public long getCacheExpire() { 101 | return cacheExpire; 102 | } 103 | 104 | public long getCacheMove() { 105 | return cacheMove; 106 | } 107 | 108 | public long getCacheTotalEntries() { 109 | return cacheTotalEntries; 110 | } 111 | 112 | public long getStorageUsed() { 113 | return storageUsed; 114 | } 115 | 116 | public long getStorageDirty() { 117 | return storageDirty; 118 | } 119 | 120 | public long getStorageCapacity() { 121 | return storageCapacity; 122 | } 123 | 124 | public long getStorageUsedBlocks() { 125 | return storageUsedBlocks; 126 | } 127 | 128 | public long getStorageFreeBlocks() { 129 | return storageFreeBlocks; 130 | } 131 | 132 | public long getStorageTotalBlocks() { 133 | return storageTotalBlocks; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import com.spring2go.bigcache.storage.StorageManager; 4 | 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | */ 9 | public class CacheConfig { 10 | private int concurrencyLevel = BigCache.DEFAULT_CONCURRENCY_LEVEL; 11 | private int capacityPerBlock = StorageManager.DEFAULT_CAPACITY_PER_BLOCK; 12 | private int initialNumberOfBlocks = StorageManager.DEFAULT_INITIAL_NUMBER_OF_BLOCKS; 13 | private long purgeInterval = BigCache.DEFAULT_PURGE_INTERVAL; 14 | private long mergeInterval = BigCache.DEFAULT_MERGE_INTERVAL; 15 | private double dirtyRatioThreshold = BigCache.DEFAULT_DIRTY_RATIO_THRESHOLD; 16 | private long maxOffHeapMemorySize = StorageManager.DEFAULT_MAX_OFFHEAP_MEMORY_SIZE; 17 | private StorageMode storageMode = StorageMode.PureFile; 18 | 19 | public int getConcurrencyLevel() { 20 | return concurrencyLevel; 21 | } 22 | 23 | public CacheConfig setConcurrencyLevel(int concurrencyLevel) { 24 | if(concurrencyLevel > 11 || concurrencyLevel < 0){ 25 | throw new IllegalArgumentException("concurrencyLevel must be between 0 and 11 inclusive!"); 26 | } 27 | 28 | this.concurrencyLevel = concurrencyLevel; 29 | return this; 30 | } 31 | 32 | public int getCapacityPerBlock() { 33 | return capacityPerBlock; 34 | } 35 | 36 | public CacheConfig setCapacityPerBlock(int capacityPerBlock) { 37 | if(capacityPerBlock < 16 * 1024 * 1024){ 38 | throw new IllegalArgumentException("capacityPerBlock must be bigger than 16MB!"); 39 | } 40 | 41 | this.capacityPerBlock = capacityPerBlock; 42 | return this; 43 | } 44 | 45 | public int getInitialNumberOfBlocks() { 46 | return initialNumberOfBlocks; 47 | } 48 | 49 | public CacheConfig setInitialNumberOfBlocks(int initialNumberOfBlocks) { 50 | 51 | if(initialNumberOfBlocks <= 0){ 52 | throw new IllegalArgumentException("initialNumberOfBlocks must be > 0!"); 53 | } 54 | 55 | this.initialNumberOfBlocks = initialNumberOfBlocks; 56 | return this; 57 | } 58 | 59 | public long getPurgeInterval() { 60 | return purgeInterval; 61 | } 62 | 63 | public CacheConfig setPurgeInterval(long purgeInterval) { 64 | this.purgeInterval = purgeInterval; 65 | return this; 66 | } 67 | 68 | public long getMergeInterval() { 69 | return mergeInterval; 70 | } 71 | 72 | public CacheConfig setMergeInterval(long mergeInterval) { 73 | this.mergeInterval = mergeInterval; 74 | return this; 75 | } 76 | 77 | public double getDirtyRatioThreshold() { 78 | return dirtyRatioThreshold; 79 | } 80 | 81 | public CacheConfig setDirtyRatioLimit(double dirtyRatioThreshold) { 82 | this.dirtyRatioThreshold = dirtyRatioThreshold; 83 | return this; 84 | } 85 | 86 | public StorageMode getStorageMode() { 87 | return storageMode; 88 | } 89 | 90 | public CacheConfig setStorageMode(StorageMode storageMode) { 91 | this.storageMode = storageMode; 92 | return this; 93 | } 94 | 95 | /** 96 | * Limiting Offheap memory usage. 97 | * 98 | * Only takes effect when the {@link StorageMode} is set to MemoryMappedPlusFile or OffHeapPlusFile mode, 99 | * in these cases, this setting limits the max offheap memory size. 100 | * 101 | * @param maxOffHeapMemorySize max offheap memory size allowed, unit : byte. 102 | * @return CacheConfig 103 | */ 104 | public CacheConfig setMaxOffHeapMemorySize(long maxOffHeapMemorySize) { 105 | if (maxOffHeapMemorySize < this.capacityPerBlock) { 106 | throw new IllegalArgumentException("maxOffHeapMemorySize must be equal to or larger than capacityPerBlock" + this.getCapacityPerBlock()); 107 | } 108 | this.maxOffHeapMemorySize = maxOffHeapMemorySize; 109 | return this; 110 | } 111 | 112 | public long getMaxOffHeapMemorySize() { 113 | return this.maxOffHeapMemorySize; 114 | } 115 | 116 | public enum StorageMode { 117 | PureFile, 118 | MemoryMappedPlusFile, 119 | OffHeapPlusFile, 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/CacheValueWrapper.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import com.spring2go.bigcache.storage.Pointer; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | * 8 | * Wrapper class in BigCache, which contains info on access time, ttl and storage. 9 | * 10 | * The {@link BigCache} will protect the r/w operation on this object by two means: 11 | * 1. use a striped write lock in its write operations. 12 | * 2. synchronize the CacheValueWrapper object in the read operations of Cache, as there may be 13 | * multiple threads working on this simultaneously. 14 | */ 15 | public class CacheValueWrapper { 16 | /** 17 | * The backend storage info of this entry. 18 | * 19 | */ 20 | protected Pointer pointer; 21 | 22 | /** 23 | * The access time in milliseconds. 24 | * 25 | * Read/write of this field should be guarded by locks. 26 | */ 27 | protected long lastAccessTime = -1; // -1 means for not initialized. 28 | 29 | /** Time to idle in milliseconds */ 30 | protected long timeToIdle = -1L; 31 | 32 | /** 33 | * Gets the last access time. 34 | * 35 | * @return the access time 36 | */ 37 | public long getLastAccessTime() { 38 | return lastAccessTime; 39 | } 40 | 41 | public CacheValueWrapper(Pointer pointer, long lastAccessTime, long timeToIdle) { 42 | this.pointer = pointer; 43 | this.lastAccessTime = lastAccessTime; 44 | this.timeToIdle = timeToIdle; 45 | } 46 | 47 | public CacheValueWrapper() { 48 | } 49 | 50 | /** 51 | * Sets the last access time. 52 | * 53 | * we need to put the following restriction: 54 | * 1. We will always set the time to a bigger value than the current one. 55 | * 2. If the pointer has expired, don't set the value, so there won't be expire to non-expire, which 56 | * is wrong 57 | * 58 | * @param accessTime the new access time 59 | * @return true if we has modified the time successfully. 60 | */ 61 | public void setLastAccessTime(long accessTime) { 62 | if (lastAccessTime < 0) { 63 | // not initialized yet. 64 | lastAccessTime = accessTime; 65 | return; 66 | } 67 | 68 | // don't set it to an old value 69 | if (lastAccessTime >= accessTime) return; 70 | 71 | // can't update the access value if it has already expired. 72 | if (isExpired()) return; 73 | 74 | lastAccessTime = accessTime; 75 | } 76 | 77 | /** 78 | * Gets the time to idle in milliseconds 79 | * 80 | * @return time to idle 81 | */ 82 | public long getTimeToIdle() { 83 | return timeToIdle; 84 | } 85 | 86 | /** 87 | * Sets the time to idle in milliseconds 88 | * 89 | * @param timeToIdle the new time to idle 90 | */ 91 | public void setTimeToIdle(long timeToIdle) { 92 | this.timeToIdle = timeToIdle; 93 | } 94 | 95 | public Pointer getPointer() { 96 | return pointer; 97 | } 98 | 99 | public void setPointer(Pointer pointer) { 100 | this.pointer = pointer; 101 | } 102 | 103 | /** 104 | * Is the cached item expired 105 | * 106 | * @return expired or not 107 | */ 108 | public boolean isExpired() { 109 | if (this.timeToIdle <= 0) return false; // never expire 110 | if (this.lastAccessTime < 0) return false; // not initialized 111 | return System.currentTimeMillis() - this.lastAccessTime > this.timeToIdle; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/ICache.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | * 9 | * The Interface ICache. 10 | * 11 | * @param the key type 12 | */ 13 | public interface ICache extends Closeable { 14 | /** 15 | * Puts the value with the specified key. 16 | * 17 | * @param key the key 18 | * @param value the value 19 | * @throws IOException 20 | */ 21 | void put(K key, byte[] value) throws IOException; 22 | 23 | /** 24 | * Puts the value with specified key and time to idle in milliseconds. 25 | * 26 | * @param key the key 27 | * @param value the value 28 | * @param ttl the time to idle value in milliseconds 29 | * @throws IOException 30 | */ 31 | void put(K key, byte[] value, long tti) throws IOException; 32 | 33 | /** 34 | * Gets the value with the specified key. 35 | * 36 | * @param key the key 37 | * @return the value 38 | * @throws IOException 39 | */ 40 | byte[] get(K key) throws IOException; 41 | 42 | /** 43 | * Delete the value with the specified key. 44 | * 45 | * @param key the key 46 | * @return the value 47 | * @throws IOException 48 | */ 49 | byte[] delete(K key) throws IOException; 50 | 51 | /** 52 | * Check if Cache contains the specified key. 53 | * 54 | * @param key the key 55 | * @return true, if successful 56 | */ 57 | boolean contains(K key); 58 | 59 | /** 60 | * Clear the cache. 61 | */ 62 | void clear(); 63 | 64 | /** 65 | * Calculates the Hit ratio. 66 | * 67 | * @return the double 68 | */ 69 | double hitRatio(); 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/lock/StripedReadWriteLock.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.lock; 2 | 3 | import java.util.concurrent.locks.ReentrantReadWriteLock; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | * 8 | * The StripedReadWriteLock is lock holder that contains array of 9 | * {@link java.util.concurrent.locks.ReentrantReadWriteLock}, on which lock/unlock 10 | * operations are performed. Purpose of this is to decrease lock contention. 11 | * When a lock requested, this lock gives a lock associated with the given id. 12 | */ 13 | public class StripedReadWriteLock { 14 | 15 | private final ReentrantReadWriteLock[] locks; 16 | 17 | /** 18 | * Default factor, creates 16 locks 19 | */ 20 | public StripedReadWriteLock() { 21 | this(4); 22 | } 23 | 24 | /** 25 | * Creates array of locks, size of array may be any from set {2^1, 2^2, ..., 2^11} 26 | * 27 | * @param storagePower size of array will be equal to 2^storagePower 28 | */ 29 | public StripedReadWriteLock(int storagePower) { 30 | if (!(storagePower >= 1 && storagePower <= 11)) { 31 | throw new IllegalArgumentException("storage power must be in {1..11}"); 32 | } 33 | 34 | int lockSize = (int) Math.pow(2, storagePower); 35 | locks = new ReentrantReadWriteLock[lockSize]; 36 | for (int i = 0; i < locks.length; i++){ 37 | locks[i] = new ReentrantReadWriteLock(); 38 | } 39 | } 40 | 41 | /** 42 | * Locks lock associated with given id. 43 | * 44 | * @param id value, from which lock is derived 45 | */ 46 | public void readLock(int id) { 47 | getLock(id).readLock().lock(); 48 | } 49 | 50 | /** 51 | * Unlocks lock associated with given id. 52 | * 53 | * @param id value, from which lock is derived 54 | */ 55 | public void readUnlock(int id) { 56 | getLock(id).readLock().unlock(); 57 | } 58 | 59 | /** 60 | * Locks lock associated with given id. 61 | * 62 | * @param id value, from which lock is derived 63 | */ 64 | public void writeLock(int id) { 65 | getLock(id).writeLock().lock(); 66 | } 67 | 68 | /** 69 | * Unlocks lock associated with given id. 70 | * 71 | * @param id value, from which lock is derived 72 | */ 73 | public void writeUnlock(int id) { 74 | getLock(id).writeLock().unlock(); 75 | } 76 | 77 | /** 78 | * Locks all locks as write lock. 79 | */ 80 | public void writeLockForAll() { 81 | for (int i = 0; i < locks.length; i++) { 82 | getLock(i).writeLock().lock(); 83 | } 84 | } 85 | 86 | /** 87 | * Unlocks all locks as write lock. 88 | */ 89 | public void writeUnlockForAll() { 90 | for (int i = 0; i < locks.length; i++) { 91 | getLock(i).writeLock().unlock(); 92 | } 93 | } 94 | 95 | /** 96 | * Finds the lock associated with the id 97 | * 98 | * @param id value, from which lock is derived 99 | * @return lock which is associated with the id 100 | */ 101 | public ReentrantReadWriteLock getLock(int id) { 102 | // locks.length-1 is a string of ones since lock.length is power of 2, 103 | // thus ending cancels out the higher bits of id and leaves the lower bits 104 | // to determine the lock. 105 | return locks[id & (locks.length - 1)]; 106 | } 107 | } -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/AlignOffHeapStorage.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import java.io.IOException; 4 | import java.lang.reflect.Field; 5 | import java.nio.ByteBuffer; 6 | 7 | /** 8 | * Created on Jul, 2020 by @author bobo 9 | * 10 | * This class is called when buffers should be allocated aligned. 11 | * 12 | * It creates a block of memory aligned to memory page size, and limits the amount of buffer each processor can access. 13 | * 14 | */ 15 | public class AlignOffHeapStorage extends OffHeapStorage { 16 | public AlignOffHeapStorage(int capacity) { 17 | super(capacity, ByteBuffer.allocateDirect(capacity)); 18 | } 19 | 20 | @Override 21 | public void close() throws IOException { 22 | if (!disposed.compareAndSet(false, true)) 23 | return; 24 | if (byteBuffer == null) 25 | return; 26 | try { 27 | Field cleanerField = byteBuffer.getClass().getDeclaredField("cleaner"); 28 | cleanerField.setAccessible(true); 29 | sun.misc.Cleaner cleaner = (sun.misc.Cleaner) cleanerField.get(byteBuffer); 30 | cleaner.clean(); 31 | } catch (Exception e) { 32 | throw new Error(e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/FileChannelStorage.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.ByteBuffer; 7 | import java.nio.channels.FileChannel; 8 | 9 | /** 10 | * Created on Jul, 2020 by @author bobo 11 | */ 12 | public class FileChannelStorage implements IStorage { 13 | private FileChannel fileChannel; 14 | private RandomAccessFile raf; 15 | 16 | public FileChannelStorage(String dir, int index, int capacity) throws IOException { 17 | File dirFile = new File(dir); 18 | if (!dirFile.exists()) { dirFile.mkdirs(); } 19 | String fullFileName = dir + index + "-" + System.currentTimeMillis() + DATA_FILE_SUFFIX; 20 | raf = new RandomAccessFile(fullFileName, "rw"); 21 | raf.setLength(capacity); 22 | fileChannel = raf.getChannel(); 23 | } 24 | 25 | @Override 26 | public void get(int position, byte[] dest) throws IOException { 27 | fileChannel.read(ByteBuffer.wrap(dest), position); 28 | } 29 | 30 | @Override 31 | public void put(int position, byte[] source) throws IOException { 32 | fileChannel.write(ByteBuffer.wrap(source), position); 33 | } 34 | 35 | @Override 36 | public void free() { 37 | // nothing to do here 38 | } 39 | 40 | @Override 41 | public void close() throws IOException { 42 | if (this.fileChannel != null) { 43 | this.fileChannel.close(); 44 | } 45 | if (this.raf != null) { 46 | this.raf.close(); 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/IStorage.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | * 9 | * The Interface IStorage for get/put cached data in bytes. 10 | */ 11 | public interface IStorage extends Closeable { 12 | public static final String DATA_FILE_SUFFIX = ".data"; 13 | 14 | /** 15 | * Gets bytes from the specified location. 16 | * 17 | * @param position the position 18 | * @param dest the destination 19 | */ 20 | void get(int position, byte[] dest) throws IOException; 21 | 22 | /** 23 | * Puts source to the specified location of the Storage. 24 | * 25 | * @param position the position 26 | * @param source the source 27 | */ 28 | void put(int position, byte[] source) throws IOException; 29 | 30 | /** 31 | * Frees the storage. 32 | */ 33 | void free(); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/IStorageBlock.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | * 9 | * The Interface IStorageBlock. 10 | * 11 | * A storage unit with fixed capacity. 12 | */ 13 | public interface IStorageBlock extends Comparable, Closeable { 14 | /** 15 | * Retrieves the payload associated with the pointer and always update the access time. 16 | * 17 | * @param pointer the pointer 18 | * @return the byte[] 19 | * @throws IOException 20 | */ 21 | byte[] retrieve(Pointer pointer) throws IOException; 22 | 23 | /** 24 | * Removes the payload and marks the used space as dirty. 25 | * 26 | * @param pointer the pointer 27 | * @return the byte[] 28 | * @throws IOException 29 | */ 30 | byte[] remove(Pointer pointer) throws IOException; 31 | 32 | /** 33 | * Removes the payload without returning the payload 34 | * 35 | * @param pointer the pointer 36 | * @throws IOException 37 | */ 38 | void removeLight(Pointer pointer) throws IOException; 39 | 40 | /** 41 | * Stores the payload. 42 | * 43 | * @param payload the payload 44 | * @return the pointer 45 | * @throws IOException 46 | */ 47 | Pointer store(byte[] payload) throws IOException; 48 | 49 | /** 50 | * Updates the payload by marking exSpace as dirty. 51 | * 52 | * @param pointer the pointer 53 | * @param payload the payload 54 | * @return the pointer 55 | * @throws IOException 56 | */ 57 | Pointer update(Pointer pointer, byte[] payload) throws IOException; 58 | 59 | /** 60 | * Calculates and returns total size of the dirty space. 61 | * 62 | * @return the total size of the dirty space. 63 | */ 64 | long getDirty(); 65 | 66 | /** 67 | * Calculates and returns total size of the used space. 68 | * 69 | * @return the total size of the used space. 70 | */ 71 | long getUsed(); 72 | 73 | /** 74 | * Calculates and returns total capacity of the block. 75 | * 76 | * @return the total capacity of the block. 77 | */ 78 | long getCapacity(); 79 | 80 | /** 81 | * Calculates and returns the dirty to capacity ratio 82 | * 83 | * @return dirty ratio 84 | */ 85 | double getDirtyRatio(); 86 | 87 | /** 88 | * Get the index of this storage block 89 | * 90 | * @return an index 91 | */ 92 | int getIndex(); 93 | 94 | /** 95 | * Frees the storage. 96 | */ 97 | void free(); 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/MemoryMappedStorage.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.ByteBuffer; 7 | import java.nio.MappedByteBuffer; 8 | import java.nio.channels.FileChannel; 9 | 10 | /** 11 | * Created on Jul, 2020 by @author bobo 12 | */ 13 | public class MemoryMappedStorage implements IStorage { 14 | private RandomAccessFile raf; 15 | private ThreadLocalByteBuffer threadLocalBuffer; 16 | 17 | public MemoryMappedStorage(String dir, int index, int capacity) throws IOException { 18 | File backFile = new File(dir); 19 | if (!backFile.exists()) { 20 | backFile.mkdirs(); 21 | } 22 | String backFileName = dir + index + "-" + System.currentTimeMillis() + DATA_FILE_SUFFIX; 23 | raf = new RandomAccessFile(backFileName, "rw"); 24 | MappedByteBuffer mappedByteBuffer = raf.getChannel().map(FileChannel.MapMode.PRIVATE, 0, capacity); 25 | threadLocalBuffer = new ThreadLocalByteBuffer(mappedByteBuffer); 26 | } 27 | 28 | private ByteBuffer getLocal(int position) { 29 | ByteBuffer buffer = threadLocalBuffer.get(); 30 | buffer.position(position); 31 | return buffer; 32 | } 33 | 34 | @Override 35 | public void close() throws IOException { 36 | if (raf != null) { 37 | raf.close(); 38 | } 39 | //implies system GC 40 | threadLocalBuffer.set(null); 41 | threadLocalBuffer = null; 42 | } 43 | 44 | @Override 45 | public void get(int position, byte[] dest) throws IOException { 46 | ByteBuffer buffer = this.getLocal(position); 47 | buffer.get(dest); 48 | } 49 | 50 | @Override 51 | public void put(int position, byte[] source) throws IOException { 52 | ByteBuffer buffer = this.getLocal(position); 53 | buffer.put(source); 54 | } 55 | 56 | @Override 57 | public void free() { 58 | MappedByteBuffer buffer = (MappedByteBuffer) threadLocalBuffer.getSourceBuffer(); 59 | buffer.clear(); 60 | } 61 | 62 | private static class ThreadLocalByteBuffer extends ThreadLocal { 63 | private ByteBuffer _src; 64 | 65 | public ThreadLocalByteBuffer(ByteBuffer src) { 66 | _src = src; 67 | } 68 | 69 | public ByteBuffer getSourceBuffer() { 70 | return _src; 71 | } 72 | 73 | @Override 74 | protected synchronized ByteBuffer initialValue() { 75 | ByteBuffer dup = _src.duplicate(); 76 | return dup; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/OffHeapStorage.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import sun.misc.Unsafe; 4 | 5 | import java.io.IOException; 6 | import java.lang.reflect.Field; 7 | import java.lang.reflect.Method; 8 | import java.nio.ByteBuffer; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | 11 | /** 12 | * Created on Jul, 2020 by @author bobo 13 | */ 14 | public class OffHeapStorage implements IStorage { 15 | protected final AtomicBoolean disposed = new AtomicBoolean(false); 16 | protected ByteBuffer byteBuffer; 17 | 18 | private static final Unsafe UNSAFE = getUnsafe(); 19 | private static final long BYTE_ARRAY_OFFSET = (long) UNSAFE.arrayBaseOffset(byte[].class); 20 | 21 | private final long address; 22 | 23 | private static Unsafe getUnsafe() { 24 | try { 25 | Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); 26 | unsafeField.setAccessible(true); 27 | return (sun.misc.Unsafe) unsafeField.get(null); 28 | } catch (Exception e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | 33 | public OffHeapStorage(int capacity) { 34 | this.address = UNSAFE.allocateMemory(capacity); 35 | } 36 | 37 | public OffHeapStorage(int capacity, ByteBuffer buffer) { 38 | this.byteBuffer = ByteBuffer.allocateDirect(capacity); 39 | try { 40 | Method method = byteBuffer.getClass().getDeclaredMethod("address"); 41 | method.setAccessible(true); 42 | this.address = (Long) method.invoke(byteBuffer); 43 | } catch (Exception e) { 44 | throw new RuntimeException("Unable to allocate offheap memory using sun.misc.Unsafe on your platform", e); 45 | } 46 | } 47 | 48 | @Override 49 | public void close() throws IOException { 50 | if (!disposed.compareAndSet(false, true)) 51 | return; 52 | UNSAFE.freeMemory(address); 53 | } 54 | 55 | @Override 56 | public void get(int position, byte[] dest) throws IOException { 57 | assert !disposed.get() : "disposed"; 58 | assert position >= 0 : position; 59 | this.get(address + position, dest, BYTE_ARRAY_OFFSET, dest.length); 60 | } 61 | 62 | /** 63 | * Get bytes from the local buffer to a given byte array. 64 | * 65 | * @param baseAddress the absolute base address of the local buffer 66 | * @param dest the dest byte array 67 | * @param destOffset the offset of the dest byte array 68 | * @param length the length of bytes to get 69 | */ 70 | private void get(long baseAddress, byte[] dest, long destOffset, long length) { 71 | UNSAFE.copyMemory(null, baseAddress, dest, destOffset, length); 72 | } 73 | 74 | @Override 75 | public void put(int position, byte[] source) throws IOException { 76 | assert !disposed.get() : "disposed"; 77 | assert position >= 0 : position; 78 | this.put(BYTE_ARRAY_OFFSET, source, address + position, source.length); 79 | 80 | } 81 | 82 | /** 83 | * Put bytes from a given byte array to the local buffer. 84 | * 85 | * @param srcOffset the offset of the source byte array 86 | * @param source the source byte array 87 | * @param baseAddress the absolute base address of the local buffer 88 | * @param length the length of bytes to put 89 | */ 90 | private void put(long srcOffset, byte[] source, long baseAddress, long length) { 91 | UNSAFE.copyMemory(source, srcOffset, null, baseAddress, length); 92 | } 93 | 94 | @Override 95 | public void free() { 96 | //do nothing 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/Pointer.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | /** 4 | * Created on Jul, 2020 by @author bobo 5 | * 6 | * The Class Pointer is a pointer to the stored cache data, which keeps 7 | * position and length of the payload and associated StorageBlock. 8 | */ 9 | public class Pointer { 10 | /** The position. */ 11 | protected int position; 12 | 13 | /** The length of the value. */ 14 | protected int length; 15 | 16 | /** The associated storage block. */ 17 | protected StorageBlock storageBlock; 18 | 19 | /** 20 | * Instantiates a new pointer. 21 | * 22 | * @param position the position 23 | * @param length the length of the value 24 | * @param storageBlock the persistent cache storage 25 | */ 26 | public Pointer(int position, int length, StorageBlock storageBlock) { 27 | this.position = position; 28 | this.length = length; 29 | this.storageBlock = storageBlock; 30 | } 31 | 32 | /** 33 | * Gets the position. 34 | * 35 | * @return the position 36 | */ 37 | public int getPosition() { 38 | return position; 39 | } 40 | 41 | /** 42 | * Sets the position. 43 | * 44 | * @param position the new position 45 | */ 46 | public void setPosition(int position) { 47 | this.position = position; 48 | } 49 | 50 | /** 51 | * Gets the storage block. 52 | * 53 | * @return the storage block. 54 | */ 55 | public StorageBlock getStorageBlock() { 56 | return storageBlock; 57 | } 58 | 59 | /** 60 | * Sets the storage block. 61 | * 62 | * @param storageBlock the new storage block. 63 | */ 64 | public void setStorageBlock(StorageBlock storageBlock) { 65 | this.storageBlock = storageBlock; 66 | } 67 | 68 | /** 69 | * Gets the length of the value 70 | * 71 | * @return the length of the stored value 72 | */ 73 | public int getLength() { 74 | return length; 75 | } 76 | 77 | /** 78 | * Sets the length of the stored value. 79 | * 80 | * @param length the length of the stored value. 81 | */ 82 | public void setLength(int length) { 83 | this.length = length; 84 | } 85 | 86 | 87 | /** 88 | * Copies given pointer. 89 | * 90 | * @param pointer the pointer 91 | * @return the pointer 92 | */ 93 | public Pointer copy(Pointer pointer) { 94 | this.position = pointer.position; 95 | this.length = pointer.length; 96 | this.storageBlock = pointer.storageBlock; 97 | return this; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/StorageBlock.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import java.io.IOException; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | import com.spring2go.bigcache.CacheConfig.StorageMode; 7 | 8 | /** 9 | * Created on Jul, 2020 by @author bobo 10 | * 11 | * The Class StorageBlock. 12 | */ 13 | public class StorageBlock implements IStorageBlock { 14 | /** The index. */ 15 | private final int index; 16 | 17 | /** The capacity. */ 18 | private final int capacity; 19 | 20 | /** The underlying storage. */ 21 | private IStorage underlyingStorage; 22 | 23 | /** The offset within the storage block. */ 24 | private final AtomicInteger currentOffset = new AtomicInteger(0); 25 | 26 | /** The dirty storage. */ 27 | private final AtomicInteger dirtyStorage = new AtomicInteger(0); 28 | 29 | /** The used storage. */ 30 | private final AtomicInteger usedStorage = new AtomicInteger(0); 31 | 32 | /** 33 | * Instantiates a new storage block. 34 | * 35 | * @param dir the directory 36 | * @param index the index 37 | * @param capacity the capacity 38 | * @throws IOException exception throws when failing to create the storage block 39 | */ 40 | public StorageBlock(String dir, int index, int capacity, StorageMode storageMode) throws IOException{ 41 | this.index = index; 42 | this.capacity = capacity; 43 | switch (storageMode) { 44 | case PureFile: 45 | underlyingStorage = new FileChannelStorage(dir, index, capacity); 46 | break; 47 | case MemoryMappedPlusFile: 48 | underlyingStorage = new MemoryMappedStorage(dir, index, capacity); 49 | break; 50 | case OffHeapPlusFile: 51 | underlyingStorage = new OffHeapStorage(capacity); 52 | break; 53 | } 54 | } 55 | 56 | @Override 57 | public byte[] retrieve(Pointer pointer) throws IOException { 58 | byte [] payload = new byte[pointer.getLength()]; 59 | underlyingStorage.get(pointer.getPosition(), payload); 60 | return payload; 61 | } 62 | 63 | @Override 64 | public byte[] remove(Pointer pointer) throws IOException { 65 | byte [] payload = retrieve(pointer); 66 | dirtyStorage.addAndGet(pointer.getLength()); 67 | usedStorage.addAndGet(-1 * pointer.getLength()); 68 | return payload; 69 | } 70 | 71 | 72 | @Override 73 | public void removeLight(Pointer pointer) throws IOException { 74 | dirtyStorage.addAndGet(pointer.getLength()); 75 | usedStorage.addAndGet(-1 * pointer.getLength()); 76 | } 77 | 78 | @Override 79 | public Pointer store(byte[] payload) throws IOException { 80 | Allocation allocation = allocate(payload); 81 | if (allocation == null) return null; // not enough storage available 82 | Pointer pointer = store(allocation, payload); 83 | return pointer; 84 | } 85 | 86 | /** 87 | * Allocates storage for the payload, return null if not enough storage available. 88 | * 89 | * @param payload the payload 90 | * @return the allocation 91 | */ 92 | protected Allocation allocate(byte[] payload) { 93 | int payloadLength = payload.length; 94 | int allocationOffset = currentOffset.addAndGet(payloadLength); 95 | if(this.capacity < allocationOffset){ 96 | return null; 97 | } 98 | Allocation allocation = new Allocation(allocationOffset - payloadLength, payloadLength); 99 | return allocation; 100 | } 101 | 102 | 103 | /** 104 | * Stores the payload by the help of allocation. 105 | * 106 | * @param allocation the allocation 107 | * @param payload the payload 108 | * @return the pointer 109 | * @throws IOException 110 | */ 111 | public Pointer store(Allocation allocation, byte[] payload) throws IOException { 112 | Pointer pointer = new Pointer(allocation.getOffset(), allocation.getLength(), this); 113 | underlyingStorage.put(allocation.getOffset(), payload); 114 | usedStorage.addAndGet(payload.length); 115 | return pointer; 116 | } 117 | 118 | @Override 119 | public Pointer update(Pointer pointer, byte[] payload) throws IOException { 120 | if (pointer.getLength() >= payload.length) { // has enough space to reuse 121 | dirtyStorage.addAndGet(pointer.getLength() - payload.length); 122 | usedStorage.addAndGet(-1 * pointer.getLength()); 123 | Allocation allocation = new Allocation(pointer.getPosition(), payload.length); 124 | return store(allocation, payload); // should always return a new pointer 125 | } else { // make a move 126 | dirtyStorage.addAndGet(pointer.getLength()); 127 | usedStorage.addAndGet(-1 * pointer.getLength()); 128 | return store(payload); // may return null because not enough space available 129 | } 130 | } 131 | 132 | @Override 133 | public long getDirty() { 134 | return this.dirtyStorage.get(); 135 | } 136 | 137 | @Override 138 | public double getDirtyRatio() { 139 | return (this.getDirty() * 1.0) / this.getCapacity(); 140 | } 141 | 142 | @Override 143 | public long getCapacity() { 144 | return capacity; 145 | } 146 | 147 | @Override 148 | public long getUsed() { 149 | return this.usedStorage.get(); 150 | } 151 | 152 | @Override 153 | public void free() { 154 | 155 | currentOffset.set(0); 156 | dirtyStorage.set(0); 157 | usedStorage.set(0); 158 | 159 | underlyingStorage.free(); 160 | } 161 | 162 | /** 163 | * The Class Allocation. 164 | */ 165 | private static class Allocation { 166 | 167 | /** The offset. */ 168 | private int offset; 169 | 170 | /** The length. */ 171 | private int length; 172 | 173 | /** 174 | * Instantiates a new allocation. 175 | * 176 | * @param offset the offset 177 | * @param length the length 178 | */ 179 | public Allocation(int offset, int length) { 180 | this.offset = offset; 181 | this.length = length; 182 | } 183 | 184 | /** 185 | * Gets the offset. 186 | * 187 | * @return the offset 188 | */ 189 | public int getOffset() { 190 | return offset; 191 | } 192 | 193 | /** 194 | * Gets the length. 195 | * 196 | * @return the length 197 | */ 198 | public int getLength() { 199 | return length; 200 | } 201 | } 202 | 203 | /** 204 | * Gets the index. 205 | * 206 | * @return the index 207 | */ 208 | public int getIndex() { 209 | return index; 210 | } 211 | 212 | @Override 213 | public void close() throws IOException { 214 | if (this.underlyingStorage != null) { 215 | this.underlyingStorage.close(); 216 | } 217 | } 218 | 219 | @Override 220 | public int compareTo(IStorageBlock o) { 221 | if (this.getIndex() < o.getIndex()) return -1; 222 | else if (this.getIndex() == o.getIndex()) return 0; 223 | else return 1; 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/storage/StorageManager.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import java.io.IOException; 4 | import java.util.HashSet; 5 | import java.util.Iterator; 6 | import java.util.Queue; 7 | import java.util.Set; 8 | import java.util.concurrent.ConcurrentLinkedQueue; 9 | import java.util.concurrent.PriorityBlockingQueue; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | import java.util.concurrent.locks.Lock; 12 | import java.util.concurrent.locks.ReentrantLock; 13 | 14 | import com.spring2go.bigcache.CacheConfig.StorageMode; 15 | 16 | /** 17 | * Created on Jul, 2020 by @author bobo 18 | * 19 | * Managing a list of used/free storage blocks for cache operations like get/put/delete 20 | * 21 | */ 22 | public class StorageManager implements IStorageBlock { 23 | /** keep track of the number of blocks allocated */ 24 | private final AtomicInteger blockCount = new AtomicInteger(0); 25 | 26 | /** 27 | * Directory for cache data store 28 | */ 29 | private final String dir; 30 | 31 | /** 32 | * The capacity per block in bytes 33 | * 34 | */ 35 | private final int capacityPerBlock; 36 | 37 | 38 | /** The active storage block change lock. */ 39 | private final Lock activeBlockChangeLock = new ReentrantLock(); 40 | 41 | /** 42 | * A list of used storage blocks 43 | */ 44 | private final Queue usedBlocks = new ConcurrentLinkedQueue(); 45 | 46 | /** 47 | * A queue of free storage blocks which is a priority queue and always return the block with smallest index. 48 | */ 49 | private final Queue freeBlocks = new PriorityBlockingQueue(); 50 | 51 | /** 52 | * Current active block for appending new cache data 53 | */ 54 | private volatile IStorageBlock activeBlock; 55 | 56 | /** 57 | * Current storage mode 58 | */ 59 | private final StorageMode storageMode; 60 | 61 | /** 62 | * The number of memory blocks allow to be created. 63 | */ 64 | private int allowedOffHeapModeBlockCount; 65 | 66 | /** 67 | * The Constant DEFAULT_CAPACITY_PER_BLOCK. 68 | */ 69 | public final static int DEFAULT_CAPACITY_PER_BLOCK = 128 * 1024 * 1024; // 128M 70 | 71 | /** The Constant DEFAULT_INITIAL_NUMBER_OF_BLOCKS. */ 72 | public final static int DEFAULT_INITIAL_NUMBER_OF_BLOCKS = 8; // 1GB total 73 | 74 | /** 75 | * The Constant DEFAULT_MEMORY_SIZE. 76 | */ 77 | public static final long DEFAULT_MAX_OFFHEAP_MEMORY_SIZE = 2 * 1024 * 1024 * 1024L; //Unit: GB 78 | 79 | public StorageManager(String dir, int capacityPerBlock, int initialNumberOfBlocks, StorageMode storageMode, 80 | long maxOffHeapMemorySize) throws IOException { 81 | 82 | if (storageMode != StorageMode.PureFile) { 83 | this.allowedOffHeapModeBlockCount = (int)(maxOffHeapMemorySize / capacityPerBlock); 84 | } else { 85 | this.allowedOffHeapModeBlockCount = 0; 86 | } 87 | this.storageMode = storageMode; 88 | this.capacityPerBlock = capacityPerBlock; 89 | this.dir = dir; 90 | 91 | for (int i = 0; i < initialNumberOfBlocks; i++) { 92 | IStorageBlock storageBlock = this.createNewBlock(i); 93 | freeBlocks.offer(storageBlock); 94 | } 95 | 96 | this.blockCount.set(initialNumberOfBlocks); 97 | this.activeBlock = freeBlocks.poll(); 98 | this.usedBlocks.add(this.activeBlock); 99 | 100 | } 101 | 102 | @Override 103 | public byte[] retrieve(Pointer pointer) throws IOException { 104 | return pointer.getStorageBlock().retrieve(pointer); 105 | } 106 | 107 | @Override 108 | public byte[] remove(Pointer pointer) throws IOException { 109 | return pointer.getStorageBlock().remove(pointer); 110 | } 111 | 112 | 113 | @Override 114 | public void removeLight(Pointer pointer) throws IOException { 115 | pointer.getStorageBlock().removeLight(pointer); 116 | } 117 | 118 | @Override 119 | public Pointer store(byte[] payload) throws IOException { 120 | Pointer pointer = activeBlock.store(payload); 121 | if (pointer != null) return pointer; // success 122 | else { // overflow 123 | activeBlockChangeLock.lock(); 124 | try { 125 | // other thread may have changed the active block 126 | pointer = activeBlock.store(payload); 127 | if (pointer != null) return pointer; // success 128 | else { // still overflow 129 | IStorageBlock freeBlock = this.freeBlocks.poll(); 130 | if (freeBlock == null) { // create a new one 131 | freeBlock = this.createNewBlock(this.blockCount.getAndIncrement()); 132 | } 133 | pointer = freeBlock.store(payload); 134 | this.activeBlock = freeBlock; 135 | this.usedBlocks.add(this.activeBlock); 136 | return pointer; 137 | } 138 | 139 | } finally { 140 | activeBlockChangeLock.unlock(); 141 | } 142 | } 143 | } 144 | 145 | /** 146 | * Stores the payload to the free storage block excluding the given block. 147 | * 148 | * @param payload the payload 149 | * @param exludingBlock the storage block to be excluded 150 | * @return the pointer 151 | */ 152 | public Pointer storeExcluding(byte[] payload, StorageBlock exludingBlock) throws IOException { 153 | while (this.activeBlock == exludingBlock) { 154 | activeBlockChangeLock.lock(); 155 | try { 156 | // other thread may have changed the active block 157 | if (this.activeBlock != exludingBlock) break; 158 | IStorageBlock freeBlock = this.freeBlocks.poll(); 159 | if (freeBlock == null) { 160 | freeBlock = this.createNewBlock(this.blockCount.getAndIncrement()); 161 | } 162 | this.activeBlock = freeBlock; 163 | this.usedBlocks.add(this.activeBlock); 164 | } finally { 165 | activeBlockChangeLock.unlock(); 166 | } 167 | } 168 | return store(payload); 169 | } 170 | 171 | @Override 172 | public Pointer update(Pointer pointer, byte[] payload) throws IOException { 173 | Pointer updatePointer = pointer.getStorageBlock().update(pointer, payload); 174 | if (updatePointer != null) { 175 | return updatePointer; 176 | } 177 | return store(payload); 178 | } 179 | 180 | @Override 181 | public long getDirty() { 182 | long dirtyStorage = 0; 183 | for(IStorageBlock block : usedBlocks) { 184 | dirtyStorage += block.getDirty(); 185 | } 186 | return dirtyStorage; 187 | } 188 | 189 | private Set getAllInUsedBlocks() { 190 | Set allBlocks = new HashSet(); 191 | allBlocks.addAll(usedBlocks); 192 | allBlocks.addAll(freeBlocks); 193 | return allBlocks; 194 | } 195 | 196 | @Override 197 | public long getCapacity() { 198 | long totalCapacity = 0; 199 | for(IStorageBlock block : getAllInUsedBlocks()) { 200 | totalCapacity += block.getCapacity(); 201 | } 202 | return totalCapacity; 203 | } 204 | 205 | @Override 206 | public double getDirtyRatio() { 207 | return (this.getDirty() * 1.0) / this.getCapacity(); 208 | } 209 | 210 | 211 | @Override 212 | public long getUsed() { 213 | long usedStorage = 0; 214 | for(IStorageBlock block : usedBlocks) { 215 | usedStorage += block.getUsed(); 216 | } 217 | return usedStorage; 218 | } 219 | 220 | @Override 221 | public void free() { 222 | // safe? 223 | for(IStorageBlock storageBlock : usedBlocks) { 224 | storageBlock.free(); 225 | this.freeBlocks.offer(storageBlock); 226 | } 227 | usedBlocks.clear(); 228 | this.activeBlock = freeBlocks.poll(); 229 | this.usedBlocks.add(this.activeBlock); 230 | } 231 | 232 | private IStorageBlock createNewBlock(int index) throws IOException { 233 | if (this.allowedOffHeapModeBlockCount > 0) { 234 | IStorageBlock block = new StorageBlock(this.dir, index, this.capacityPerBlock, this.storageMode); 235 | this.allowedOffHeapModeBlockCount--; 236 | return block; 237 | } else { 238 | return new StorageBlock(this.dir, index, this.capacityPerBlock, StorageMode.PureFile); 239 | } 240 | } 241 | 242 | // only run by one thread. 243 | public void clean() { 244 | synchronized (this) { 245 | Iterator it = usedBlocks.iterator(); 246 | while(it.hasNext()) { 247 | IStorageBlock storageBlock = it.next(); 248 | if (storageBlock == activeBlock) { 249 | // let active block be cleaned in the next run 250 | continue; 251 | } 252 | 253 | if (storageBlock.getUsed() == 0) { 254 | // we will not allocating memory from it any more and it is used by nobody. 255 | storageBlock.free(); 256 | freeBlocks.add(storageBlock); 257 | it.remove(); 258 | } 259 | } 260 | } 261 | } 262 | 263 | @Override 264 | public void close() throws IOException { 265 | for(IStorageBlock usedBlock : usedBlocks) { 266 | usedBlock.close(); 267 | } 268 | usedBlocks.clear(); 269 | for(IStorageBlock freeBlock : freeBlocks) { 270 | freeBlock.close(); 271 | } 272 | freeBlocks.clear(); 273 | } 274 | 275 | @Override 276 | public int compareTo(IStorageBlock o) { 277 | throw new IllegalStateException("Not implemented!"); 278 | } 279 | 280 | @Override 281 | public int getIndex() { 282 | throw new IllegalStateException("Not implemented!"); 283 | } 284 | 285 | public int getFreeBlockCount() { 286 | return this.freeBlocks.size(); 287 | } 288 | 289 | public int getUsedBlockCount() { 290 | return this.usedBlocks.size(); 291 | } 292 | 293 | public int getTotalBlockCount() { 294 | return this.getAllInUsedBlocks().size(); 295 | } 296 | 297 | } 298 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/bigcache/utils/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.utils; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | 8 | /** 9 | * Created on Jul, 2020 by @author bobo 10 | */ 11 | public class FileUtil { 12 | private static final int BUFFER_SIZE = 4096 * 4; 13 | 14 | /** 15 | * Only check if a given filename is valid according to the OS rules. 16 | * 17 | * You still need to handle other failures when actually creating 18 | * the file (e.g. insufficient permissions, lack of drive space, security restrictions). 19 | * @param file the name of a file 20 | * @return true if the file is valid, false otherwise 21 | */ 22 | public static boolean isFilenameValid(String file) { 23 | File f = new File(file); 24 | try { 25 | f.getCanonicalPath(); 26 | return true; 27 | } catch (IOException e) { 28 | return false; 29 | } 30 | } 31 | 32 | public static void deleteDirectory(File dir) { 33 | if (!dir.exists()) return; 34 | File[] subs = dir.listFiles(); 35 | if (subs != null) { 36 | for (File f : dir.listFiles()) { 37 | if (f.isFile()) { 38 | if(!f.delete()) { 39 | throw new IllegalStateException("delete file failed: "+f); 40 | } 41 | } else { 42 | deleteDirectory(f); 43 | } 44 | } 45 | } 46 | if(!dir.delete()) { 47 | throw new IllegalStateException("delete directory failed: "+dir); 48 | } 49 | } 50 | 51 | public static void deleteFile(File file) { 52 | if (!file.exists() || !file.isFile()) { 53 | return; 54 | } 55 | if (!file.delete()) { 56 | throw new IllegalStateException("delete file failed: "+file); 57 | } 58 | } 59 | 60 | /** 61 | * Copy a directory and all of its contents. 62 | * 63 | * @param from source file 64 | * @param to target file 65 | * @return success or failure 66 | */ 67 | public static boolean copyDirectory(File from, File to) { 68 | return copyDirectory(from, to, (byte[]) null); 69 | } 70 | 71 | public static boolean copyDirectory(String from, String to) { 72 | return copyDirectory(new File(from), new File(to)); 73 | } 74 | 75 | public static boolean copyDirectory(File from, File to, byte[] buffer) { 76 | if (from == null) return false; 77 | if (!from.exists()) return true; 78 | if (!from.isDirectory()) return false; 79 | if (to.exists()) return false; 80 | if (!to.mkdirs()) return false; 81 | 82 | String[] list = from.list(); 83 | // Some JVMs return null for File.list() when the directory is empty. 84 | if (list != null) { 85 | if (buffer == null) buffer = new byte[BUFFER_SIZE]; // return this buffer to copy files 86 | 87 | for(int i = 0; i < list.length; i++) { 88 | String fileName = list[i]; 89 | 90 | File entry = new File(from, fileName); 91 | 92 | if (entry.isDirectory()) { 93 | if (!copyDirectory(entry, new File(to, fileName), buffer)) { 94 | return false; 95 | } 96 | } 97 | else { 98 | if (!copyFile(entry, new File(to, fileName), buffer)) { 99 | return false; 100 | } 101 | } 102 | } 103 | } 104 | 105 | return true; 106 | } 107 | 108 | public static boolean copyFile(File from, File to, byte[] buf) { 109 | if (buf == null) buf = new byte[BUFFER_SIZE]; 110 | 111 | FileInputStream from_s = null; 112 | FileOutputStream to_s = null; 113 | 114 | try { 115 | from_s = new FileInputStream(from); 116 | to_s = new FileOutputStream(to); 117 | 118 | for(int bytesRead = from_s.read(buf); bytesRead > 0; bytesRead = from_s.read(buf)) { 119 | to_s.write(buf, 0, bytesRead); 120 | } 121 | 122 | to_s.getFD().sync(); 123 | 124 | } catch (IOException ioe) { 125 | return false; 126 | } finally { 127 | if (from_s != null) { 128 | try { 129 | from_s.close(); 130 | from_s = null; 131 | } catch (IOException ioe) { 132 | 133 | } 134 | } 135 | if (to_s != null) { 136 | try { 137 | to_s.close(); 138 | to_s = null; 139 | } catch (IOException ioe) { 140 | } 141 | } 142 | } 143 | 144 | return true; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/lrucache/v1/LruCacheV1.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.lrucache.v1; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * LruCache实现,V1版本,线程不安全 8 | * 9 | * Created on Jul, 2020 by @author bobo 10 | */ 11 | public class LruCacheV1 { 12 | 13 | private int maxCapacity; 14 | private Map> map; 15 | private Node head, tail; 16 | 17 | private static class Node { 18 | private V value; 19 | private K key; 20 | private Node next, prev; 21 | 22 | public Node(K key, V value) { 23 | this.key = key; 24 | this.value = value; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return value.toString(); 30 | } 31 | } 32 | 33 | // 从双向链表中移除一个节点 34 | private void removeNode(Node node) { 35 | if (node == null) return; 36 | 37 | if (node.prev != null) { 38 | node.prev.next = node.next; 39 | } else { 40 | head = node.next; 41 | } 42 | 43 | if (node.next != null) { 44 | node.next.prev = node.prev; 45 | } else { 46 | tail = node.prev; 47 | } 48 | } 49 | 50 | // 向双向链表的尾部添加一个节点 51 | private void offerNode(Node node) { 52 | if (node == null) return; 53 | 54 | if (head == null) { 55 | head = tail = node; 56 | } else { 57 | tail.next = node; 58 | node.prev = tail; 59 | node.next = null; 60 | tail = node; 61 | } 62 | } 63 | 64 | public LruCacheV1(final int maxCapacity) { 65 | this.maxCapacity = maxCapacity; 66 | map = new HashMap<>(); 67 | } 68 | 69 | public void put(K key, V value) { 70 | if (map.containsKey(key)) { 71 | Node node = map.get(key); 72 | node.value = value; 73 | removeNode(node); 74 | offerNode(node); 75 | } else { 76 | if (map.size() == maxCapacity) { 77 | map.remove(head.key); 78 | removeNode(head); 79 | } 80 | Node node = new Node<>(key, value); 81 | offerNode(node); 82 | map.put(key, node); 83 | } 84 | } 85 | 86 | public V get(K key) { 87 | Node node = map.get(key); 88 | if (node == null) return null; 89 | removeNode(node); 90 | offerNode(node); 91 | return node.value; 92 | } 93 | 94 | public int size() { 95 | return map.size(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/lrucache/v2/LruCacheV2.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.lrucache.v2; 2 | 3 | 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.concurrent.locks.Lock; 7 | import java.util.concurrent.locks.ReadWriteLock; 8 | import java.util.concurrent.locks.ReentrantReadWriteLock; 9 | 10 | /** 11 | * LruCache实现,V1版本,线程安全 12 | * 13 | * Created on Jul, 2020 by @author bobo 14 | */ 15 | public class LruCacheV2 { 16 | private int maxCapacity; 17 | private Map> map; 18 | private Node head, tail; 19 | 20 | private ReadWriteLock lock = new ReentrantReadWriteLock(); 21 | private Lock writeLock = lock.writeLock(); 22 | private Lock readLock = lock.readLock(); 23 | 24 | private static class Node { 25 | private V value; 26 | private K key; 27 | private Node next, prev; 28 | 29 | public Node(K key, V value) { 30 | this.key = key; 31 | this.value = value; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return value.toString(); 37 | } 38 | } 39 | 40 | // 从双向链表中移除一个节点 41 | private void removeNode(Node node) { 42 | if (node == null) return; 43 | 44 | if (node.prev != null) { 45 | node.prev.next = node.next; 46 | } else { 47 | head = node.next; 48 | } 49 | 50 | if (node.next != null) { 51 | node.next.prev = node.prev; 52 | } else { 53 | tail = node.prev; 54 | } 55 | } 56 | 57 | // 向双向链表的尾部添加一个节点 58 | private void offerNode(Node node) { 59 | if (node == null) return; 60 | 61 | if (head == null) { 62 | head = tail = node; 63 | } else { 64 | tail.next = node; 65 | node.prev = tail; 66 | node.next = null; 67 | tail = node; 68 | } 69 | } 70 | 71 | public LruCacheV2(final int maxCapacity) { 72 | this.maxCapacity = maxCapacity; 73 | map = new HashMap<>(); 74 | } 75 | 76 | public void put(K key, V value) { 77 | writeLock.lock(); 78 | try { 79 | if (map.containsKey(key)) { 80 | Node node = map.get(key); 81 | node.value = value; 82 | removeNode(node); 83 | offerNode(node); 84 | } else { 85 | if (map.size() >= maxCapacity) { 86 | map.remove(head.key); 87 | removeNode(head); 88 | } 89 | Node node = new Node<>(key, value); 90 | offerNode(node); 91 | map.put(key, node); 92 | } 93 | } finally { 94 | writeLock.unlock(); 95 | } 96 | } 97 | 98 | public V get(K key) { 99 | writeLock.lock(); 100 | try { 101 | Node node = map.get(key); 102 | if (node == null) return null; 103 | removeNode(node); 104 | offerNode(node); 105 | return node.value; 106 | } finally { 107 | writeLock.unlock(); 108 | } 109 | } 110 | 111 | public int size() { 112 | readLock.lock(); 113 | try { 114 | return map.size(); 115 | } finally { 116 | readLock.unlock(); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/lrucache/v3/LruCacheV3.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.lrucache.v3; 2 | 3 | import com.spring2go.lrucache.v2.LruCacheV2; 4 | 5 | /** 6 | * LruCache实现,V3版本,线程安全+高并发 7 | * 8 | * Created on Jul, 2020 by @author bobo 9 | */ 10 | public class LruCacheV3 { 11 | 12 | private LruCacheV2[] cacheSegments; 13 | 14 | public LruCacheV3(final int maxCapacity) { 15 | int cores = Runtime.getRuntime().availableProcessors(); 16 | int concurrency = cores < 2 ? 2 : cores; 17 | cacheSegments = new LruCacheV2[concurrency]; 18 | int segmentCapacity = maxCapacity / concurrency; 19 | if (maxCapacity % concurrency == 1) segmentCapacity++; 20 | for (int index = 0; index < cacheSegments.length; index++) { 21 | cacheSegments[index] = new LruCacheV2<>(segmentCapacity); 22 | } 23 | } 24 | 25 | public LruCacheV3(final int concurrency, final int maxCapacity) { 26 | cacheSegments = new LruCacheV2[concurrency]; 27 | int segmentCapacity = maxCapacity / concurrency; 28 | if (maxCapacity % concurrency == 1) segmentCapacity++; 29 | for (int index = 0; index < cacheSegments.length; index++) { 30 | cacheSegments[index] = new LruCacheV2<>(segmentCapacity); 31 | } 32 | } 33 | 34 | private int segmentIndex(K key) { 35 | int hashCode = Math.abs(key.hashCode() * 31); 36 | return hashCode % cacheSegments.length; 37 | } 38 | 39 | private LruCacheV2 cache(K key) { 40 | return cacheSegments[segmentIndex(key)]; 41 | } 42 | 43 | public void put(K key, V value) { 44 | cache(key).put(key, value); 45 | } 46 | 47 | public V get(K key) { 48 | return cache(key).get(key); 49 | } 50 | 51 | public int size() { 52 | int size = 0; 53 | for (LruCacheV2 cache : cacheSegments) { 54 | size += cache.size(); 55 | } 56 | return size; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/AbstractDeque.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.util.Collection; 4 | import java.util.Deque; 5 | import java.util.Iterator; 6 | import java.util.NoSuchElementException; 7 | 8 | /** 9 | * Created on Jul, 2020 by @author bobo 10 | */ 11 | public abstract class AbstractDeque implements Deque { 12 | protected AbstractDeque() { 13 | } 14 | 15 | @Override 16 | public void addFirst(E e) { 17 | if (!offerFirst(e)) 18 | throw new IllegalStateException("Queue full"); 19 | 20 | } 21 | 22 | @Override 23 | public void addLast(E e) { 24 | if (!offerLast(e)) 25 | throw new IllegalStateException("Queue full"); 26 | } 27 | 28 | @Override 29 | public E removeFirst() { 30 | E x = pollFirst(); 31 | if (x != null) 32 | return x; 33 | else 34 | throw new NoSuchElementException(); 35 | } 36 | 37 | @Override 38 | public E removeLast() { 39 | E x = pollLast(); 40 | if (x != null) 41 | return x; 42 | else 43 | throw new NoSuchElementException(); 44 | } 45 | 46 | @Override 47 | public E getFirst() { 48 | E x = peekFirst(); 49 | if (x != null) 50 | return x; 51 | else 52 | throw new NoSuchElementException(); 53 | } 54 | 55 | @Override 56 | public E getLast() { 57 | E x = peekLast(); 58 | if (x != null) 59 | return x; 60 | else 61 | throw new NoSuchElementException(); 62 | } 63 | 64 | @Override 65 | public boolean removeFirstOccurrence(Object o) { 66 | throw new UnsupportedOperationException(); 67 | } 68 | 69 | @Override 70 | public boolean removeLastOccurrence(Object o) { 71 | throw new UnsupportedOperationException(); 72 | } 73 | 74 | @Override 75 | public boolean add(E e) { 76 | if (offer(e)) 77 | return true; 78 | else 79 | throw new IllegalStateException("Queue full"); 80 | } 81 | 82 | @Override 83 | public boolean offer(E e) { 84 | return offerLast(e); 85 | } 86 | 87 | @Override 88 | public E remove() { 89 | E x = poll(); 90 | if (x != null) 91 | return x; 92 | else 93 | throw new NoSuchElementException(); 94 | } 95 | 96 | @Override 97 | public E poll() { 98 | return pollFirst(); 99 | } 100 | 101 | @Override 102 | public E element() { 103 | E x = peek(); 104 | if (x != null) 105 | return x; 106 | else 107 | throw new NoSuchElementException(); 108 | } 109 | 110 | @Override 111 | public E peek() { 112 | return peekFirst(); 113 | } 114 | 115 | @Override 116 | public void push(E e) { 117 | addFirst(e); 118 | } 119 | 120 | @Override 121 | public E pop() { 122 | return removeFirst(); 123 | } 124 | 125 | @Override 126 | public boolean remove(Object o) { 127 | return removeFirstOccurrence(o); 128 | } 129 | 130 | @Override 131 | public boolean containsAll(Collection c) { 132 | throw new UnsupportedOperationException(); 133 | } 134 | 135 | @Override 136 | public boolean addAll(Collection c) { 137 | throw new UnsupportedOperationException(); 138 | } 139 | 140 | @Override 141 | public boolean removeAll(Collection c) { 142 | throw new UnsupportedOperationException(); 143 | } 144 | 145 | @Override 146 | public boolean retainAll(Collection c) { 147 | throw new UnsupportedOperationException(); 148 | } 149 | 150 | @Override 151 | public void clear() { 152 | while (poll() != null) 153 | ; 154 | } 155 | 156 | @Override 157 | public boolean contains(Object o) { 158 | throw new UnsupportedOperationException(); 159 | } 160 | 161 | @Override 162 | public int size() { 163 | throw new UnsupportedOperationException(); 164 | } 165 | 166 | @Override 167 | public boolean isEmpty() { 168 | throw new UnsupportedOperationException(); 169 | } 170 | 171 | @Override 172 | public Iterator iterator() { 173 | throw new UnsupportedOperationException(); 174 | } 175 | 176 | @Override 177 | public Object[] toArray() { 178 | throw new UnsupportedOperationException(); 179 | } 180 | 181 | @Override 182 | public T[] toArray(T[] a) { 183 | throw new UnsupportedOperationException(); 184 | } 185 | 186 | @Override 187 | public Iterator descendingIterator() { 188 | throw new UnsupportedOperationException(); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/BehindStore.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | */ 8 | public interface BehindStore extends CacheStore { 9 | 10 | void setSyncInterval(long syncInterval); 11 | Stats prevStats(); 12 | void prevStats(Stats stats); 13 | public static class Stats { 14 | long updateCount; 15 | long updateCost; 16 | 17 | long removedCount; 18 | long removedCost; 19 | 20 | long purgedCount; 21 | long purgedCost; 22 | 23 | public Stats(long updateCount, long updateCost, long removedCount, long removedCost, long purgedCount, long purgedCost) { 24 | this.updateCount = updateCount; 25 | this.updateCost = updateCost; 26 | this.removedCount = removedCount; 27 | this.removedCost = removedCost; 28 | this.purgedCount = purgedCount; 29 | this.purgedCost = purgedCost; 30 | } 31 | 32 | public Stats plus(Stats other) { 33 | return new Stats( 34 | Math.max(0, updateCount + other.getUpdateCount()), 35 | Math.max(0, updateCost + other.getUpdateCost()), 36 | Math.max(0, removedCount + other.getRemovedCount()), 37 | Math.max(0, removedCost + other.getRemovedCost()), 38 | Math.max(0, purgedCount + other.getPurgedCount()), 39 | Math.max(0, purgedCost + other.getPurgedCost()) 40 | ); 41 | } 42 | 43 | public Stats minus(Stats other) { 44 | return new Stats( 45 | Math.max(0, updateCount - other.getUpdateCount()), 46 | Math.max(0, updateCost - other.getUpdateCost()), 47 | Math.max(0, removedCount - other.getRemovedCount()), 48 | Math.max(0, removedCost - other.getRemovedCost()), 49 | Math.max(0, purgedCount - other.getPurgedCount()), 50 | Math.max(0, purgedCost - other.getPurgedCost()) 51 | ); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return "Stats{" + 57 | "updateCount=" + updateCount + 58 | ", updateCost=" + updateCost + 59 | ", removedCount=" + removedCount + 60 | ", removedCost=" + removedCost + 61 | ", purgedCount=" + purgedCount + 62 | ", purgedCost=" + purgedCost + 63 | '}'; 64 | } 65 | 66 | public long getUpdateCount() { 67 | return updateCount; 68 | } 69 | 70 | public long getUpdateCost() { 71 | return updateCost; 72 | } 73 | 74 | public long getRemovedCount() { 75 | return removedCount; 76 | } 77 | 78 | public long getRemovedCost() { 79 | return removedCost; 80 | } 81 | 82 | public long getPurgedCount() { 83 | return purgedCount; 84 | } 85 | 86 | public long getPurgedCost() { 87 | return purgedCost; 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/Cache.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Serializable; 4 | import java.util.Map; 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | */ 9 | public interface Cache { 10 | V get(K key); 11 | 12 | V get(K key, AccessLevel level); 13 | 14 | V get(K key, UpdateTimestamp strategy); 15 | 16 | V get(K key, AccessLevel level, UpdateTimestamp strategy); 17 | 18 | V put(K key, V value); 19 | 20 | V put(K key, V value, ExpirationPolicy expirationPolicy); 21 | 22 | V putIfAbsent(K key, V value); 23 | 24 | V putIfAbsent(K key, V value, ExpirationPolicy expirationPolicy); 25 | 26 | void putAll(Map map); 27 | 28 | void putAll(Map map, ExpirationPolicy expirationPolicy); 29 | 30 | V replace(K key, V value); 31 | 32 | boolean replace(K key, V oldValue, V newValue); 33 | 34 | V remove(K key); 35 | 36 | boolean remove(K key, V value); 37 | 38 | boolean contains(K key); 39 | 40 | CacheStats stats(); 41 | 42 | void close(); 43 | 44 | enum AccessLevel{ 45 | MEMORY(0), 46 | EVICT_STORE(1), 47 | BEHIND_STORE(2); 48 | 49 | private final int code; 50 | AccessLevel(int code) { 51 | this.code = code; 52 | } 53 | 54 | public int getCode() { 55 | return code; 56 | } 57 | 58 | public boolean accessEvictStore(){ 59 | return this.code >= EVICT_STORE.getCode(); 60 | } 61 | 62 | public boolean accessBehindStore(){ 63 | return this.code >= BEHIND_STORE.getCode(); 64 | } 65 | } 66 | 67 | enum UpdateTimestamp { 68 | Access(0), 69 | Write(1), 70 | Create(2); 71 | 72 | private final int code; 73 | UpdateTimestamp(int code) { 74 | this.code = code; 75 | } 76 | 77 | public int getCode() { 78 | return code; 79 | } 80 | 81 | public boolean updateAccessTime(){ 82 | return this.code >= Access.getCode(); 83 | } 84 | 85 | public boolean updateWriteTime(){ 86 | return this.code >= Write.getCode(); 87 | } 88 | 89 | public boolean updateCreateTime(){ 90 | return this.code >= Create.getCode(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/CacheBuilder.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import com.spring2go.okcache.impl.CacheImpl; 9 | 10 | /** 11 | * Created on Jul, 2020 by @author bobo 12 | */ 13 | public class CacheBuilder { 14 | private int initCapacity = 1; 15 | private long maximumSize = 2000; 16 | private int concurrencyLevel = 1; 17 | 18 | private long expireAfterAccess = -1l; 19 | private long expireAfterWrite = -1l; 20 | private long expireAfterCreate = -1l; 21 | 22 | private float evictionStartFactor = 0.85f; 23 | private float evictionStopFactor = 0.70f; 24 | 25 | private final List> evictStores = new ArrayList>(); 26 | private final List> writeBehindStores = new ArrayList>(); 27 | 28 | public CacheBuilder initialCapacity(int initialCapacity) { 29 | checkArgument(initialCapacity >= 0); 30 | this.initCapacity = initialCapacity; 31 | return this; 32 | } 33 | 34 | public CacheBuilder maximumSize(long size) { 35 | checkArgument(size >= 0, "maximum size must not be negative"); 36 | this.maximumSize = size; 37 | return this; 38 | } 39 | 40 | public CacheBuilder concurrencyLevel(int concurrencyLevel) { 41 | checkArgument(concurrencyLevel > 0); 42 | this.concurrencyLevel = concurrencyLevel; 43 | return this; 44 | } 45 | 46 | public CacheBuilder expireAfterAccess(long duration, TimeUnit unit) { 47 | checkArgument(duration >= 0, "duration cannot be negative:" + duration + unit); 48 | this.expireAfterAccess = unit.toMillis(duration); 49 | return this; 50 | } 51 | 52 | public CacheBuilder expireAfterWrite(long duration, TimeUnit unit) { 53 | checkArgument(duration >= 0, "duration cannot be negative:" + duration + unit); 54 | this.expireAfterWrite = unit.toMillis(duration); 55 | return this; 56 | } 57 | 58 | public CacheBuilder expireAfterCreate(long duration, TimeUnit unit) { 59 | checkArgument(duration >= 0, "duration cannot be negative:" + duration + unit); 60 | this.expireAfterCreate = unit.toMillis(duration); 61 | return this; 62 | } 63 | 64 | public CacheBuilder evictionFactors(float startFactor, float stopFactor) { 65 | checkArgument(startFactor > 0 && startFactor < 1 && stopFactor > 0 && stopFactor < 1, "eviction factors must be between 0 an 1"); 66 | checkArgument(startFactor > stopFactor, "startFactor must be great than stopFactor"); 67 | this.evictionStartFactor = startFactor; 68 | this.evictionStopFactor = stopFactor; 69 | return this; 70 | } 71 | 72 | public CacheBuilder addEvictStore(CacheStore store) { 73 | checkArgument(store!=null, "evictStore must not be null."); 74 | checkArgument(!evictStores.contains(store), "the evictStore has already added"); 75 | 76 | evictStores.add(store); 77 | return this; 78 | } 79 | 80 | public CacheBuilder addWriteBehindStore(BehindStore store) { 81 | checkArgument(store!=null, "writeBehindStore must not be null."); 82 | checkArgument(!writeBehindStores.contains(store), "the writeBehindStore has already added"); 83 | 84 | writeBehindStores.add(store); 85 | return this; 86 | } 87 | 88 | public Cache build() { 89 | return new CacheImpl(initCapacity, maximumSize, concurrencyLevel, expireAfterAccess, expireAfterWrite, expireAfterCreate, evictionStartFactor, evictionStopFactor, evictStores, writeBehindStores); 90 | } 91 | 92 | public static CacheBuilder newBuilder(){ 93 | return new CacheBuilder(); 94 | } 95 | 96 | public static CacheBuilder newBuilder(Class key, Class value){ 97 | return new CacheBuilder(); 98 | } 99 | 100 | 101 | public static void checkArgument(boolean expression) { 102 | if (!expression) { 103 | throw new IllegalArgumentException(); 104 | } 105 | } 106 | 107 | public static void checkArgument(boolean expression, Object errorMessage) { 108 | if (!expression) { 109 | throw new IllegalArgumentException(String.valueOf(errorMessage)); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/CacheEntry.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | */ 8 | public interface CacheEntry extends Serializable { 9 | K getKey(); 10 | 11 | V getValue(); 12 | 13 | void setValue(V value); 14 | 15 | /** 16 | * Returns the time that this entry was last accessed, in ms. 17 | */ 18 | long getAccessTime(); 19 | 20 | /** 21 | * Sets the entry access time in ns. 22 | */ 23 | void setAccessTime(long time); 24 | 25 | /** 26 | * Returns the time that this entry was last written, in ms. 27 | */ 28 | long getWriteTime(); 29 | 30 | /** 31 | * Sets the entry write time in ms. 32 | */ 33 | void setWriteTime(long time); 34 | 35 | /** 36 | * Returns the time that this entry was created, in ms. 37 | */ 38 | long getCreateTime(); 39 | 40 | /** 41 | * Sets the entry created time in ms. 42 | */ 43 | void setCreateTime(long time); 44 | 45 | long getExpireAfterAccess(); 46 | void setExpireAfterAccess(long time); 47 | long getExpireAfterWrite(); 48 | void setExpireAfterWrite(long time); 49 | long getExpireAfterCreate(); 50 | void setExpireAfterCreate(long time); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/CacheEntryHelper.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | */ 8 | public class CacheEntryHelper { 9 | public static boolean isExpired(CacheEntry entry, long msTime){ 10 | long t; 11 | if((t = entry.getExpireAfterAccess()) > 0 && (msTime - entry.getAccessTime() > t)) return true; 12 | if((t = entry.getExpireAfterWrite()) > 0 && (msTime - entry.getWriteTime() > t)) return true; 13 | if((t = entry.getExpireAfterCreate()) > 0 && (msTime - entry.getCreateTime() > t)) return true; 14 | return false; 15 | } 16 | 17 | public static long calculateTTL(CacheEntry entry, long msTime) { 18 | long ttl = Long.MAX_VALUE, t; 19 | if((t = entry.getExpireAfterAccess()) > 0) ttl = entry.getAccessTime() + t - msTime; 20 | if((t = entry.getExpireAfterWrite()) > 0) ttl = Math.min(ttl,entry.getWriteTime() + t - msTime); 21 | if((t = entry.getExpireAfterCreate()) > 0) ttl = Math.min(ttl,entry.getCreateTime() + t - msTime); 22 | return ttl; 23 | } 24 | 25 | public static long calculateExpiredTimeInMS(CacheEntry entry) { 26 | long now = TimeHelper.nowMs(); 27 | long ttl = calculateTTL(entry, now); 28 | if(ttl == Long.MAX_VALUE) return ttl; 29 | return now + ttl; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/CacheStats.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | /** 4 | * Created on Jul, 2020 by @author bobo 5 | */ 6 | public class CacheStats { 7 | private final long hitCount; 8 | private final long missCount; 9 | 10 | private final long memoryHitCount; 11 | private final long memoryMissCount; 12 | 13 | private final long createCount; 14 | private final long updateCount; 15 | private final long removeCount; 16 | 17 | private final long evictCount; 18 | private final long expireCount; 19 | 20 | private final long evictStoreHitCount; 21 | private final long evictStoreMissCount; 22 | 23 | private final long behindStoreHitCount; 24 | private final long behindStoreMissCount; 25 | 26 | private final long size; 27 | private final long memorySize; 28 | 29 | public CacheStats(long hitCount, long missCount, long memoryHitCount, long memoryMissCount, long createCount, long updateCount, long removeCount, long evictCount, long expireCount, long evictStoreHitCount, long evictStoreMissCount, long behindStoreHitCount, long behindStoreMissCount, long size, long memorySize) { 30 | this.hitCount = hitCount; 31 | this.missCount = missCount; 32 | this.memoryHitCount = memoryHitCount; 33 | this.memoryMissCount = memoryMissCount; 34 | this.createCount = createCount; 35 | this.updateCount = updateCount; 36 | this.removeCount = removeCount; 37 | this.evictCount = evictCount; 38 | this.expireCount = expireCount; 39 | this.evictStoreHitCount = evictStoreHitCount; 40 | this.evictStoreMissCount = evictStoreMissCount; 41 | this.behindStoreHitCount = behindStoreHitCount; 42 | this.behindStoreMissCount = behindStoreMissCount; 43 | this.size = size; 44 | this.memorySize = memorySize; 45 | } 46 | 47 | public CacheStats() { 48 | this(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); 49 | } 50 | 51 | public long requestCount() { 52 | return hitCount + missCount; 53 | } 54 | 55 | public double hitRate() { 56 | double r = requestCount(); 57 | return r > 0 ? hitCount / r : 1.0; 58 | } 59 | 60 | public double memoryHitRate() { 61 | double r = memoryHitCount + memoryMissCount; 62 | return r > 0 ? memoryHitCount / r : 1.0; 63 | } 64 | 65 | public double evictStoreHitRate() { 66 | double r = evictStoreHitCount + evictStoreMissCount; 67 | return r > 0 ? evictStoreHitCount / r : 1.0; 68 | } 69 | 70 | public double behindStoreHitRate() { 71 | double r = behindStoreHitCount + behindStoreMissCount; 72 | return r > 0 ? behindStoreHitCount / r : 1.0; 73 | } 74 | 75 | public long getHitCount() { 76 | return hitCount; 77 | } 78 | 79 | public long getMissCount() { 80 | return missCount; 81 | } 82 | 83 | public long getMemoryHitCount() { 84 | return memoryHitCount; 85 | } 86 | 87 | public long getMemoryMissCount() { 88 | return memoryMissCount; 89 | } 90 | 91 | public long getCreateCount() { 92 | return createCount; 93 | } 94 | 95 | public long getUpdateCount() { 96 | return updateCount; 97 | } 98 | 99 | public long getRemoveCount() { 100 | return removeCount; 101 | } 102 | 103 | public long getEvictCount() { 104 | return evictCount; 105 | } 106 | 107 | public long getExpireCount() { 108 | return expireCount; 109 | } 110 | 111 | public long getEvictStoreHitCount() { 112 | return evictStoreHitCount; 113 | } 114 | 115 | public long getEvictStoreMissCount() { 116 | return evictStoreMissCount; 117 | } 118 | 119 | public long getBehindStoreHitCount() { 120 | return behindStoreHitCount; 121 | } 122 | 123 | public long getBehindStoreMissCount() { 124 | return behindStoreMissCount; 125 | } 126 | 127 | public long getSize() { 128 | return size; 129 | } 130 | 131 | public long getMemorySize() { 132 | return memorySize; 133 | } 134 | 135 | public CacheStats minus(CacheStats other) { 136 | return new CacheStats( 137 | Math.max(0, hitCount - other.hitCount), 138 | Math.max(0, missCount - other.missCount), 139 | Math.max(0, memoryHitCount - other.memoryHitCount), 140 | Math.max(0, memoryMissCount - other.memoryMissCount), 141 | Math.max(0, createCount - other.createCount), 142 | Math.max(0, updateCount - other.updateCount), 143 | Math.max(0, removeCount - other.removeCount), 144 | Math.max(0, evictCount - other.evictCount), 145 | Math.max(0, expireCount - other.expireCount), 146 | Math.max(0, evictStoreHitCount - other.evictStoreHitCount), 147 | Math.max(0, evictStoreMissCount - other.evictStoreMissCount), 148 | Math.max(0, behindStoreHitCount - other.behindStoreHitCount), 149 | Math.max(0, behindStoreMissCount - other.behindStoreMissCount), 150 | Math.max(0, size - other.size), 151 | Math.max(0, memorySize - other.memorySize) 152 | ); 153 | } 154 | 155 | public CacheStats plus(CacheStats other) { 156 | return new CacheStats( 157 | Math.max(0, hitCount + other.hitCount), 158 | Math.max(0, missCount + other.missCount), 159 | Math.max(0, memoryHitCount + other.memoryHitCount), 160 | Math.max(0, memoryMissCount + other.memoryMissCount), 161 | Math.max(0, createCount + other.createCount), 162 | Math.max(0, updateCount + other.updateCount), 163 | Math.max(0, removeCount + other.removeCount), 164 | Math.max(0, evictCount + other.evictCount), 165 | Math.max(0, expireCount + other.expireCount), 166 | Math.max(0, evictStoreHitCount + other.evictStoreHitCount), 167 | Math.max(0, evictStoreMissCount + other.evictStoreMissCount), 168 | Math.max(0, behindStoreHitCount + other.behindStoreHitCount), 169 | Math.max(0, behindStoreMissCount + other.behindStoreMissCount), 170 | Math.max(0, size + other.size), 171 | Math.max(0, memorySize + other.memorySize) 172 | ); 173 | } 174 | 175 | @Override 176 | public String toString() { 177 | return new StringBuilder(this.getClass().getSimpleName()).append(":\n") 178 | .append("\t").append("hitCount").append(":").append(hitCount).append("\n") 179 | .append("\t").append("missCount").append(":").append(missCount).append("\n") 180 | .append("\t").append("memoryHitCount").append(":").append(memoryHitCount).append("\n") 181 | .append("\t").append("memoryMissCount").append(":").append(memoryMissCount).append("\n") 182 | .append("\t").append("createCount").append(":").append(createCount).append("\n") 183 | .append("\t").append("updateCount").append(":").append(updateCount).append("\n") 184 | .append("\t").append("removeCount").append(":").append(removeCount).append("\n") 185 | .append("\t").append("evictCount").append(":").append(evictCount).append("\n") 186 | .append("\t").append("expireCount").append(":").append(expireCount).append("\n") 187 | .append("\t").append("evictStoreHitCount").append(":").append(evictStoreHitCount).append("\n") 188 | .append("\t").append("evictStoreMissCount").append(":").append(evictStoreMissCount).append("\n") 189 | .append("\t").append("behindStoreHitCount").append(":").append(behindStoreHitCount).append("\n") 190 | .append("\t").append("behindStoreMissCount").append(":").append(behindStoreMissCount).append("\n") 191 | .append("\t").append("size").append(":").append(size).append("\n") 192 | .append("\t").append("memorySize").append(":").append(memorySize).append("\n") 193 | .toString(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/CacheStatsCounter.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.util.concurrent.atomic.AtomicLong; 4 | 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | */ 9 | public class CacheStatsCounter { 10 | final AtomicLong memoryHitCount = new AtomicLong(0); 11 | final AtomicLong memoryMissCount = new AtomicLong(0); 12 | 13 | final AtomicLong createCount = new AtomicLong(0); 14 | final AtomicLong updateCount = new AtomicLong(0); 15 | final AtomicLong removeCount = new AtomicLong(0); 16 | 17 | final AtomicLong evictCount = new AtomicLong(0); 18 | final AtomicLong expireCount = new AtomicLong(0); 19 | 20 | final AtomicLong evictStoreHitCount = new AtomicLong(0); 21 | final AtomicLong evictStoreMissCount = new AtomicLong(0); 22 | 23 | final AtomicLong behindStoreHitCount = new AtomicLong(0); 24 | final AtomicLong behindStoreMissCount = new AtomicLong(0); 25 | 26 | final AtomicLong size = new AtomicLong(0); 27 | final AtomicLong memorySize = new AtomicLong(0); 28 | 29 | public void memoryHits(int count) { 30 | memoryHitCount.addAndGet(count); 31 | } 32 | 33 | public void memoryMisses(int count) { 34 | memoryMissCount.addAndGet(count); 35 | } 36 | 37 | public void recordCreates(int count) { 38 | createCount.addAndGet(count); 39 | } 40 | 41 | public void recordUpdates(int count) { 42 | updateCount.addAndGet(count); 43 | } 44 | 45 | public void recordRemoves(int count) { 46 | removeCount.addAndGet(count); 47 | } 48 | 49 | public void recordEvicts(int count) { 50 | evictCount.addAndGet(count); 51 | } 52 | 53 | public void recordExpires(int count) { 54 | expireCount.addAndGet(count); 55 | } 56 | 57 | public void evictStoreHits(int count) { 58 | evictStoreHitCount.addAndGet(count); 59 | } 60 | 61 | public void evictStoreMisses(int count) { 62 | evictStoreMissCount.addAndGet(count); 63 | } 64 | 65 | public void behindStoreHits(int count) { 66 | behindStoreHitCount.addAndGet(count); 67 | } 68 | 69 | public void behindStoreMisses(int count) { 70 | behindStoreMissCount.addAndGet(count); 71 | } 72 | 73 | public void sizeIncrement() { 74 | size.incrementAndGet(); 75 | } 76 | 77 | public void sizeDecrement() { 78 | size.decrementAndGet(); 79 | } 80 | 81 | public void memorySizeIncrement() { 82 | memorySize.incrementAndGet(); 83 | } 84 | 85 | public void memorySizeDecrement() { 86 | memorySize.decrementAndGet(); 87 | } 88 | 89 | public CacheStats snapshot() { 90 | return new CacheStats( 91 | memoryHitCount.get() + evictStoreHitCount.get() + behindStoreHitCount.get(), 92 | memoryMissCount.get() - evictStoreHitCount.get() - behindStoreHitCount.get(), 93 | memoryHitCount.get(), 94 | memoryMissCount.get(), 95 | createCount.get(), 96 | updateCount.get(), 97 | removeCount.get(), 98 | evictCount.get(), 99 | expireCount.get(), 100 | evictStoreHitCount.get(), 101 | evictStoreMissCount.get(), 102 | behindStoreHitCount.get(), 103 | behindStoreMissCount.get(), 104 | size.get(), 105 | memorySize.get() 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/CacheStore.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | */ 8 | public interface CacheStore { 9 | void store(CacheEntry entry) throws Exception; 10 | 11 | CacheEntry load(K key) throws Exception; 12 | 13 | CacheEntry remove(K key) throws Exception; 14 | 15 | void close(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/DummyCacheStore.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Serializable; 4 | 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | */ 9 | public class DummyCacheStore implements CacheStore { 10 | 11 | @Override 12 | public void store(CacheEntry entry) { 13 | } 14 | 15 | @Override 16 | public CacheEntry load(K key) { 17 | return null; 18 | } 19 | 20 | @Override 21 | public CacheEntry remove(K key) { 22 | return null; 23 | } 24 | 25 | @Override 26 | public void close() { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/ExpirationPolicy.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | /** 4 | * Created on Jul, 2020 by @author bobo 5 | */ 6 | public class ExpirationPolicy { 7 | public static ExpirationPolicy never() { 8 | return new ExpirationPolicy(-1l, -1l, -1l); 9 | } 10 | 11 | public static ExpirationPolicy afterAccess(long ms) { 12 | return new ExpirationPolicy(ms, -1l, -1l); 13 | } 14 | 15 | public static ExpirationPolicy afterWrite(long ms) { 16 | return new ExpirationPolicy(-1l, ms, -1l); 17 | } 18 | 19 | public static ExpirationPolicy afterCreate(long ms) { 20 | return new ExpirationPolicy(-1l, -1l, ms); 21 | } 22 | 23 | public static ExpirationPolicy afterAccessOrWrite(long access, long write) { 24 | return new ExpirationPolicy(access, write, -1l); 25 | } 26 | 27 | public static ExpirationPolicy after(long access, long write, long create) { 28 | return new ExpirationPolicy(access, write, create); 29 | } 30 | 31 | long afterAccess = -1l; 32 | long afterWrite = -1l; 33 | long afterCreate = -1l; 34 | 35 | private ExpirationPolicy(long afterAccess, long afterWrite, long afterCreate) { 36 | this.afterAccess = afterAccess; 37 | this.afterWrite = afterWrite; 38 | this.afterCreate = afterCreate; 39 | } 40 | 41 | public long getAfterAccess() { 42 | return afterAccess; 43 | } 44 | 45 | public long getAfterWrite() { 46 | return afterWrite; 47 | } 48 | 49 | public long getAfterCreate() { 50 | return afterCreate; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/ICache.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | * 9 | * The Interface ICache. 10 | * 11 | * @param the key type 12 | */ 13 | public interface ICache extends Closeable { 14 | 15 | /** 16 | * Puts the value with the specified key. 17 | * 18 | * @param key the key 19 | * @param value the value 20 | * @throws IOException 21 | */ 22 | void put(K key, byte[] value) throws IOException; 23 | 24 | /** 25 | * Puts the value with specified key and time to idle in milliseconds. 26 | * 27 | * @param key the key 28 | * @param value the value 29 | * @param ttl the time to idle value in milliseconds 30 | * @throws IOException 31 | */ 32 | void put(K key, byte[] value, long tti) throws IOException; 33 | 34 | /** 35 | * Gets the value with the specified key. 36 | * 37 | * @param key the key 38 | * @return the value 39 | * @throws IOException 40 | */ 41 | byte[] get(K key) throws IOException; 42 | 43 | /** 44 | * Delete the value with the specified key. 45 | * 46 | * @param key the key 47 | * @return the value 48 | * @throws IOException 49 | */ 50 | byte[] delete(K key) throws IOException; 51 | 52 | /** 53 | * Check if Cache contains the specified key. 54 | * 55 | * @param key the key 56 | * @return true, if successful 57 | */ 58 | boolean contains(K key); 59 | 60 | /** 61 | * Clear the cache. 62 | */ 63 | void clear(); 64 | 65 | /** 66 | * Calculates the Hit ratio. 67 | * 68 | * @return the double 69 | */ 70 | double hitRatio(); 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/JavaSerializer.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.*; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | */ 8 | public class JavaSerializer implements Serializer { 9 | public byte[] serialize(Object o) throws IOException { 10 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 11 | ObjectOutputStream oos = new ObjectOutputStream(bos); 12 | oos.writeObject(o); 13 | oos.flush(); 14 | return bos.toByteArray(); 15 | } 16 | 17 | public Object deserialize(byte[] value) throws IOException, ClassNotFoundException { 18 | ByteArrayInputStream bis = new ByteArrayInputStream(value); 19 | ObjectInputStream ois = new ObjectInputStream(bis); 20 | return ois.readObject(); 21 | } 22 | 23 | @Override 24 | public K deserialize(byte[] value, Class clazz) throws Exception { 25 | return (K)deserialize(value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/Serializer.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Be responsible for serializing an object to a byte array 7 | * or deserializing an object from a byte array. 8 | * 9 | * Created on Jul, 2020 by @author bobo 10 | */ 11 | public interface Serializer { 12 | 13 | /** 14 | * Serialize an object to a byte array. 15 | * 16 | * @param o To be serialized. 17 | * @return the result of serializing. 18 | * @throws Exception 19 | */ 20 | public byte[] serialize(Object o) throws Exception; 21 | 22 | /** 23 | * Deserialize an object from a byte array. 24 | * And be able to automatically detect the class type to be deserialized. 25 | * 26 | * @param value 27 | * @return 28 | * @throws Exception 29 | */ 30 | public Object deserialize(byte[] value) throws Exception; 31 | 32 | /** 33 | * Deserialize an object from a byte array. 34 | * 35 | * @param value 36 | * @param clazz 37 | * @param 38 | * @return 39 | * @throws Exception 40 | */ 41 | public K deserialize(byte[] value, Class clazz) throws Exception; 42 | 43 | } -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/TimeHelper.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | /** 4 | * Created on Jul, 2020 by @author bobo 5 | */ 6 | public class TimeHelper { 7 | public static long nowMs(){ 8 | return System.currentTimeMillis(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/impl/CacheImpl.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache.impl; 2 | 3 | import java.io.Serializable; 4 | import java.lang.reflect.Field; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import sun.misc.Unsafe; 10 | 11 | import com.spring2go.okcache.BehindStore; 12 | import com.spring2go.okcache.Cache; 13 | import com.spring2go.okcache.CacheStats; 14 | import com.spring2go.okcache.CacheStore; 15 | import com.spring2go.okcache.ExpirationPolicy; 16 | 17 | /** 18 | * Created on Jul, 2020 by @author bobo 19 | */ 20 | public class CacheImpl implements Cache { 21 | private final List> evictStores = new ArrayList>(); 22 | private final List> writeBehindStores = new ArrayList>(); 23 | 24 | private int initCapacity = 2000; 25 | private long maximumSize = 2000; 26 | private int concurrencyLevel = 1; 27 | 28 | private long expireAfterAccess = -1l; 29 | private long expireAfterWrite = -1l; 30 | private long expireAfterCreate = -1l; 31 | 32 | private float evictionStartFactor = 0.85f; 33 | private float evictionStopFactor = 0.70f; 34 | 35 | private int segmentCount = 1; 36 | private int segmentShift; 37 | private int segmentMask = 0; 38 | private final CacheSegment[] segments; 39 | 40 | public CacheImpl(int initCapacity, long maximumSize, int concurrencyLevel, long expireAfterAccess, long expireAfterWrite, long expireAfterCreate, float evictionStartFactor, float evictionStopFactor, List> evictStores, List> writeBehindStores) { 41 | this.initCapacity = Math.min(initCapacity, (int) maximumSize); 42 | this.maximumSize = Math.max(initCapacity, maximumSize); 43 | this.concurrencyLevel = Math.max(concurrencyLevel, 1); 44 | this.expireAfterAccess = expireAfterAccess; 45 | this.expireAfterWrite = expireAfterWrite; 46 | this.expireAfterCreate = expireAfterCreate; 47 | this.evictionStartFactor = Math.max(evictionStartFactor, evictionStopFactor); 48 | this.evictionStopFactor = Math.min(evictionStopFactor, evictionStartFactor); 49 | this.evictStores.addAll(evictStores); 50 | this.writeBehindStores.addAll(writeBehindStores); 51 | 52 | int scount=1; 53 | int sshift=0; 54 | while (scount < this.concurrencyLevel) { 55 | ++sshift; 56 | scount<<=1; 57 | } 58 | this.segmentCount = scount; 59 | this.segmentShift = sshift; 60 | 61 | int init = this.initCapacity / scount + 1; 62 | long max = this.maximumSize / scount + 1; 63 | 64 | this.segmentMask = this.segmentCount - 1; 65 | segments = new CacheSegment[scount]; 66 | 67 | CacheSegment s; 68 | for (int i = 0; i < scount; i++) { 69 | s = new CacheSegment(init, max, evictionStartFactor, evictionStopFactor, evictStores, writeBehindStores); 70 | UNSAFE.putOrderedObject(segments, SBASE + (i << SSHIFT), s); 71 | } 72 | } 73 | 74 | @Override 75 | public V get(K key) { 76 | return get(key, AccessLevel.EVICT_STORE, UpdateTimestamp.Access); 77 | } 78 | 79 | @Override 80 | public V get(K key, AccessLevel level) { 81 | return get(key, level, UpdateTimestamp.Access); 82 | } 83 | 84 | @Override 85 | public V get(K key, UpdateTimestamp strategy) { 86 | return get(key, AccessLevel.EVICT_STORE, strategy); 87 | } 88 | 89 | @Override 90 | public V get(K key, AccessLevel level, UpdateTimestamp strategy) { 91 | int hash = hash(key); 92 | CacheSegment s = segmentForHash(hash); 93 | return s.get(hash, key , level, strategy); 94 | } 95 | 96 | @Override 97 | public V put(K key, V value) { 98 | return put(key, value, ExpirationPolicy.after(expireAfterAccess, expireAfterWrite, expireAfterCreate)); 99 | } 100 | 101 | @Override 102 | public V put(K key, V value, ExpirationPolicy expirationPolicy) { 103 | int hash = hash(key); 104 | if (value == null) 105 | throw new NullPointerException(); 106 | CacheSegment s = segmentForHash(hash); 107 | return s.put(hash, key, value, expirationPolicy, false); 108 | } 109 | 110 | @Override 111 | public V putIfAbsent(K key, V value) { 112 | return putIfAbsent(key, value, ExpirationPolicy.after(expireAfterAccess, expireAfterWrite, expireAfterCreate)); 113 | } 114 | 115 | @Override 116 | public V putIfAbsent(K key, V value, ExpirationPolicy expirationPolicy) { 117 | int hash = hash(key); 118 | if (value == null) 119 | throw new NullPointerException(); 120 | CacheSegment s = segmentForHash(hash); 121 | return s.put(hash, key, value, expirationPolicy, true); 122 | } 123 | 124 | @Override 125 | public void putAll(Map map) { 126 | for (Map.Entry entry : map.entrySet()) { 127 | put(entry.getKey(), entry.getValue()); 128 | } 129 | } 130 | 131 | @Override 132 | public void putAll(Map map, ExpirationPolicy expirationPolicy) { 133 | for (Map.Entry entry : map.entrySet()) { 134 | put(entry.getKey(), entry.getValue(), expirationPolicy); 135 | } 136 | } 137 | 138 | @Override 139 | public V replace(K key, V value) { 140 | int hash = hash(key); 141 | if (value == null) 142 | throw new NullPointerException(); 143 | CacheSegment s = segmentForHash(hash); 144 | return s == null ? null : s.replace(key, hash, value); 145 | } 146 | 147 | @Override 148 | public boolean replace(K key, V oldValue, V newValue) { 149 | int hash = hash(key); 150 | if (oldValue == null || newValue == null) 151 | throw new NullPointerException(); 152 | CacheSegment s = segmentForHash(hash); 153 | return s != null && s.replace(key, hash, oldValue, newValue); 154 | } 155 | 156 | @Override 157 | public V remove(K key) { 158 | int hash = hash(key); 159 | CacheSegment s = segmentForHash(hash); 160 | return s == null ? null : s.remove(key, hash, null); 161 | } 162 | 163 | @Override 164 | public boolean remove(K key, V value) { 165 | int hash = hash(key); 166 | CacheSegment s; 167 | return value != null && (s = segmentForHash(hash)) != null && 168 | s.remove(key, hash, value) != null; 169 | } 170 | 171 | @Override 172 | public boolean contains(K key) { 173 | int hash = hash(key); 174 | if (key == null) 175 | return false; 176 | CacheSegment s = segmentForHash(hash); 177 | return s.contains(hash, key); 178 | } 179 | 180 | @Override 181 | public CacheStats stats() { 182 | CacheStats stats = new CacheStats(); 183 | for (CacheSegment segament : segments) { 184 | stats = stats.plus(segament.stats()); 185 | } 186 | return stats; 187 | } 188 | 189 | @Override 190 | public void close() { 191 | for(CacheStore cs : evictStores) { 192 | cs.close(); 193 | } 194 | for(CacheStore cs : writeBehindStores) { 195 | cs.close(); 196 | } 197 | } 198 | 199 | private int hash(Object k) { 200 | int h = 0; 201 | h ^= k.hashCode(); 202 | 203 | // Spread bits to regularize both segment and index locations, 204 | // using variant of single-word Wang/Jenkins hash. 205 | h += (h << 15) ^ 0xffffcd7d; 206 | h ^= (h >>> 10); 207 | h += (h << 3); 208 | h ^= (h >>> 6); 209 | h += (h << 2) + (h << 14); 210 | return h ^ (h >>> 16); 211 | } 212 | 213 | @SuppressWarnings("unchecked") 214 | static final CacheSegment segmentAt(CacheSegment[] ss, int j) { 215 | long u = (j << SSHIFT) + SBASE; 216 | return ss == null ? null : 217 | (CacheSegment) UNSAFE.getObjectVolatile(ss, u); 218 | } 219 | 220 | 221 | // Hash-based segment and entry accesses 222 | /** 223 | * Get the segment for the given hash 224 | */ 225 | @SuppressWarnings("unchecked") 226 | private CacheSegment segmentForHash(int h) { 227 | long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; 228 | return (CacheSegment) UNSAFE.getObjectVolatile(segments, u); 229 | } 230 | 231 | // Unsafe mechanics 232 | private static final Unsafe UNSAFE; 233 | private static final long SBASE; 234 | private static final int SSHIFT; 235 | 236 | static { 237 | int ss; 238 | try { 239 | Field f = Unsafe.class.getDeclaredField("theUnsafe"); 240 | f.setAccessible(true); 241 | UNSAFE = (Unsafe) f.get(null); 242 | 243 | Class sc = CacheSegment[].class; 244 | SBASE = UNSAFE.arrayBaseOffset(sc); 245 | ss = UNSAFE.arrayIndexScale(sc); 246 | } catch (Exception e) { 247 | throw new Error(e); 248 | } 249 | if ((ss & (ss-1)) != 0) 250 | throw new Error("data type scale not a power of two"); 251 | SSHIFT = 31 - Integer.numberOfLeadingZeros(ss); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/impl/HashCacheEntry.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache.impl; 2 | 3 | import com.spring2go.okcache.CacheEntry; 4 | import com.spring2go.okcache.ExpirationPolicy; 5 | import com.spring2go.okcache.TimeHelper; 6 | import sun.misc.Unsafe; 7 | 8 | import java.io.Serializable; 9 | import java.lang.reflect.Field; 10 | 11 | /** 12 | * 13 | * Created on Jul, 2020 by @author bobo 14 | */ 15 | public class HashCacheEntry implements CacheEntry { 16 | final int hash; 17 | 18 | final K key; 19 | volatile V value; 20 | volatile long accessTime; 21 | volatile long writeTime; 22 | volatile long createTime; 23 | volatile long expireAfterAccess = -1l; 24 | volatile long expireAfterWrite = -1l; 25 | volatile long expireAfterCreate = -1l; 26 | 27 | transient volatile HashCacheEntry next = null; 28 | transient volatile HashCacheEntry previous = null; 29 | transient volatile HashCacheEntry nextAccess = null; 30 | transient volatile HashCacheEntry previousAccess = null; 31 | 32 | public HashCacheEntry(int hash, K key, V value, ExpirationPolicy expirationPolicy) { 33 | this(hash,key, value, expirationPolicy.getAfterAccess(), expirationPolicy.getAfterWrite(), expirationPolicy.getAfterCreate()); 34 | } 35 | 36 | public HashCacheEntry(int hash,K key, V value, long expireAfterAccess, long expireAfterWrite, long expireAfterCreate) { 37 | this.hash = hash; 38 | this.key = key; 39 | this.value = value; 40 | this.expireAfterAccess = expireAfterAccess; 41 | this.expireAfterWrite = expireAfterWrite; 42 | this.expireAfterCreate = expireAfterCreate; 43 | accessTime = writeTime = createTime = TimeHelper.nowMs(); 44 | } 45 | 46 | public K getKey() { 47 | return key; 48 | } 49 | 50 | public V getValue() { 51 | return value; 52 | } 53 | 54 | public void setValue(V value) { 55 | UNSAFE.putOrderedObject(this,valueOffset,value); 56 | } 57 | 58 | public long getAccessTime() { 59 | return accessTime; 60 | } 61 | 62 | public void setAccessTime(long time) { 63 | UNSAFE.putLong(this,accessTimeOffset,time); 64 | } 65 | 66 | public long getWriteTime() { 67 | return writeTime; 68 | } 69 | 70 | public void setWriteTime(long time) { 71 | UNSAFE.putLong(this,accessTimeOffset,time); 72 | UNSAFE.putLong(this,writeTimeOffset,time); 73 | } 74 | 75 | public long getCreateTime() { 76 | return createTime; 77 | } 78 | 79 | public void setCreateTime(long time) { 80 | UNSAFE.putLong(this,accessTimeOffset,time); 81 | UNSAFE.putLong(this,writeTimeOffset,time); 82 | UNSAFE.putLong(this,createTimeOffset,time); 83 | } 84 | 85 | public long getExpireAfterAccess() { 86 | return expireAfterAccess; 87 | } 88 | 89 | public void setExpireAfterAccess(long time) { 90 | UNSAFE.putLong(this,expireAfterAccessOffset,time); 91 | } 92 | 93 | public long getExpireAfterWrite() { 94 | return expireAfterWrite; 95 | } 96 | 97 | public void setExpireAfterWrite(long time) { 98 | UNSAFE.putLong(this,expireAfterWriteOffset,time); 99 | } 100 | 101 | public long getExpireAfterCreate() { 102 | return expireAfterCreate; 103 | } 104 | 105 | public void setExpireAfterCreate(long time) { 106 | UNSAFE.putLong(this,expireAfterCreateOffset,time); 107 | } 108 | 109 | public HashCacheEntry getNext() { 110 | return next; 111 | } 112 | 113 | public void setNext(HashCacheEntry next) { 114 | UNSAFE.putOrderedObject(this, nextOffset, next); 115 | } 116 | 117 | public HashCacheEntry getPrevious() { 118 | return previous; 119 | } 120 | 121 | public void setPrevious(HashCacheEntry previous) { 122 | UNSAFE.putOrderedObject(this, previousOffset, next); 123 | } 124 | 125 | public HashCacheEntry getNextInAccessQueue() { 126 | return nextAccess; 127 | } 128 | 129 | public void setNextInAccessQueue(HashCacheEntry next) { 130 | UNSAFE.putOrderedObject(this, nextAccessOffset,next); 131 | } 132 | 133 | public HashCacheEntry getPreviousInAccessQueue() { 134 | return previousAccess; 135 | } 136 | 137 | public void setPreviousInAccessQueue(HashCacheEntry previous) { 138 | UNSAFE.putOrderedObject(this, previousAccessOffset, previous); 139 | } 140 | 141 | static final Unsafe UNSAFE; 142 | static final long valueOffset; 143 | static final long accessTimeOffset; 144 | static final long writeTimeOffset; 145 | static final long createTimeOffset; 146 | static final long expireAfterAccessOffset; 147 | static final long expireAfterWriteOffset; 148 | static final long expireAfterCreateOffset; 149 | 150 | static final long nextOffset; 151 | static final long previousOffset; 152 | static final long nextAccessOffset; 153 | static final long previousAccessOffset; 154 | 155 | static { 156 | try { 157 | Field f = Unsafe.class.getDeclaredField("theUnsafe"); 158 | f.setAccessible(true); 159 | UNSAFE = (Unsafe) f.get(null); 160 | 161 | Class k = HashCacheEntry.class; 162 | valueOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("value")); 163 | accessTimeOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("accessTime")); 164 | writeTimeOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("writeTime")); 165 | createTimeOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("createTime")); 166 | expireAfterAccessOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("expireAfterAccess")); 167 | expireAfterWriteOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("expireAfterWrite")); 168 | expireAfterCreateOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("expireAfterCreate")); 169 | 170 | nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next")); 171 | previousOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("previous")); 172 | 173 | nextAccessOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("nextAccess")); 174 | previousAccessOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("previousAccess")); 175 | 176 | } catch (Exception e) { 177 | throw new Error(e); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/impl/SegmentAccessQueue.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache.impl; 2 | 3 | import com.spring2go.okcache.AbstractDeque; 4 | 5 | import java.util.Iterator; 6 | import java.util.NoSuchElementException; 7 | 8 | /** 9 | * 10 | * Created on Jul, 2020 by @author bobo 11 | */ 12 | public class SegmentAccessQueue extends AbstractDeque { 13 | @Override 14 | public boolean offerFirst(HashCacheEntry entry) { 15 | if (entry == null) return false; 16 | 17 | if (entry.getNextInAccessQueue() != null) 18 | connectAccessOrder(entry.getPreviousInAccessQueue(), entry.getNextInAccessQueue()); 19 | 20 | HashCacheEntry headNext = head.getNextInAccessQueue(); 21 | connectAccessOrder(head, entry); 22 | connectAccessOrder(entry, headNext); 23 | 24 | return true; 25 | } 26 | 27 | @Override 28 | public boolean offerLast(HashCacheEntry entry) { 29 | if (entry == null) return false; 30 | 31 | if (entry.getNextInAccessQueue() != null) 32 | connectAccessOrder(entry.getPreviousInAccessQueue(), entry.getNextInAccessQueue()); 33 | HashCacheEntry headPrevious = head.getPreviousInAccessQueue(); 34 | connectAccessOrder(headPrevious, entry); 35 | connectAccessOrder(entry, head); 36 | 37 | return true; 38 | } 39 | 40 | @Override 41 | public HashCacheEntry pollFirst() { 42 | HashCacheEntry entry = head.getNextInAccessQueue(); 43 | if (entry == head) return null; 44 | connectAccessOrder(head, entry.getNextInAccessQueue()); 45 | nullifyAccessOrder(entry); 46 | return entry; 47 | } 48 | 49 | @Override 50 | public HashCacheEntry pollLast() { 51 | HashCacheEntry entry = head.getPreviousInAccessQueue(); 52 | if (entry == head) return null; 53 | connectAccessOrder(entry.getPreviousInAccessQueue(), head); 54 | nullifyAccessOrder(entry); 55 | return entry; 56 | } 57 | 58 | @Override 59 | public HashCacheEntry peekFirst() { 60 | HashCacheEntry entry = head.getNextInAccessQueue(); 61 | return entry == head ? null : entry; 62 | } 63 | 64 | @Override 65 | public HashCacheEntry peekLast() { 66 | HashCacheEntry entry = head.getPreviousInAccessQueue(); 67 | return entry == head ? null : entry; 68 | } 69 | 70 | @Override 71 | public boolean remove(Object o) { 72 | HashCacheEntry entry = (HashCacheEntry) o; 73 | HashCacheEntry previous = entry.getPreviousInAccessQueue(); 74 | HashCacheEntry next = entry.getNextInAccessQueue(); 75 | 76 | if (previous == null) { 77 | return false; 78 | } else { 79 | connectAccessOrder(previous, next); 80 | nullifyAccessOrder(entry); 81 | return true; 82 | } 83 | 84 | } 85 | 86 | @Override 87 | public boolean contains(Object o) { 88 | HashCacheEntry entry = (HashCacheEntry) o; 89 | return entry.getNextInAccessQueue() != null; 90 | } 91 | 92 | @Override 93 | public boolean isEmpty() { 94 | return head.getNextInAccessQueue() == head; 95 | } 96 | 97 | @Override 98 | public void clear() { 99 | HashCacheEntry e, next; 100 | e = head.getNextInAccessQueue(); 101 | while (e != head) { 102 | next = e.getNextInAccessQueue(); 103 | nullifyAccessOrder(e); 104 | e = next; 105 | } 106 | connectAccessOrder(head, head); 107 | } 108 | 109 | @Override 110 | public Iterator iterator() { 111 | return new Iterator() { 112 | private HashCacheEntry next = peek(); 113 | 114 | @Override 115 | public boolean hasNext() { 116 | return next != null; 117 | } 118 | 119 | @Override 120 | public HashCacheEntry next() { 121 | if (!hasNext()) { 122 | throw new NoSuchElementException(); 123 | } 124 | try { 125 | return next; 126 | } finally { 127 | HashCacheEntry n = next().getNextInAccessQueue(); 128 | next = n == head ? null : n; 129 | } 130 | } 131 | 132 | @Override 133 | public void remove() { 134 | throw new UnsupportedOperationException(); 135 | } 136 | }; 137 | } 138 | 139 | @Override 140 | public int size() { 141 | int size = 0; 142 | HashCacheEntry entry = head; 143 | while ((entry = entry.getNextInAccessQueue()) != head) size++; 144 | return size; 145 | } 146 | 147 | final HashCacheEntry head = new HeadHashCacheEntry(); 148 | 149 | 150 | void connectAccessOrder(HashCacheEntry previous, HashCacheEntry next) { 151 | previous.setNextInAccessQueue(next); 152 | next.setPreviousInAccessQueue(previous); 153 | } 154 | 155 | void nullifyAccessOrder(HashCacheEntry nulled) { 156 | nulled.setNextInAccessQueue(null); 157 | nulled.setPreviousInAccessQueue(null); 158 | } 159 | 160 | final static class HeadHashCacheEntry extends HashCacheEntry { 161 | HeadHashCacheEntry() { 162 | super(0,null,null,0,0,0); 163 | previousAccess = this; 164 | nextAccess = this; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/impl/SegmentStatsCounter.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache.impl; 2 | 3 | import com.spring2go.okcache.CacheStats; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | */ 8 | public class SegmentStatsCounter { 9 | volatile long hitCount; 10 | volatile long missCount; 11 | 12 | volatile long memoryHitCount = 0l; 13 | volatile long memoryMissCount = 0l; 14 | 15 | volatile long createCount = 0l; 16 | volatile long updateCount = 0l; 17 | volatile long removeCount = 0l; 18 | 19 | volatile long evictCount = 0l; 20 | volatile long expireCount = 0l; 21 | 22 | volatile long evictStoreHitCount = 0l; 23 | volatile long evictStoreMissCount = 0l; 24 | 25 | volatile long behindStoreHitCount = 0l; 26 | volatile long behindStoreMissCount = 0l; 27 | 28 | volatile long size = 0l; 29 | volatile long memorySize = 0l; 30 | 31 | volatile long evictStoreExceptionCount = 0l; 32 | volatile long behindStoreExceptionCount = 0l; 33 | 34 | public void hits(int count) { 35 | hitCount += count; 36 | } 37 | 38 | public void misses(int count) { 39 | missCount += count; 40 | } 41 | 42 | public void memoryHits(int count) { 43 | memoryHitCount += count; 44 | } 45 | 46 | public void memoryMisses(int count) { 47 | memoryMissCount += count; 48 | } 49 | 50 | public void recordCreates(int count) { 51 | createCount += count; 52 | } 53 | 54 | public void recordUpdates(int count) { 55 | updateCount += count; 56 | } 57 | 58 | public void recordRemoves(int count) { 59 | removeCount+=count; 60 | } 61 | 62 | public void recordEvicts(int count) { 63 | evictCount += count; 64 | } 65 | 66 | public void recordExpires(int count) { 67 | expireCount+=count; 68 | } 69 | 70 | public void evictStoreHits(int count) { 71 | evictStoreHitCount+=count; 72 | } 73 | 74 | public void evictStoreMisses(int count) { 75 | evictStoreMissCount+=count; 76 | } 77 | 78 | public void behindStoreHits(int count) { 79 | behindStoreHitCount+=count; 80 | } 81 | 82 | public void behindStoreMisses(int count) { 83 | behindStoreMissCount+=count; 84 | } 85 | 86 | public void sizeIncrement() { 87 | size++; 88 | } 89 | 90 | public void sizeDecrement() { 91 | size--; 92 | } 93 | 94 | public void memorySizeIncrement() { 95 | memorySize++; 96 | } 97 | 98 | public void memorySizeDecrement() { 99 | memorySize--; 100 | } 101 | 102 | public void evictStoreException(int count){ 103 | evictStoreExceptionCount += count; 104 | } 105 | 106 | public void behindStoreException(int count){ 107 | behindStoreExceptionCount += count; 108 | } 109 | 110 | public CacheStats snapshot() { 111 | return new CacheStats( 112 | hitCount, 113 | missCount, 114 | memoryHitCount, 115 | memoryMissCount, 116 | createCount, 117 | updateCount, 118 | removeCount, 119 | evictCount, 120 | expireCount, 121 | evictStoreHitCount, 122 | evictStoreMissCount, 123 | behindStoreHitCount, 124 | behindStoreMissCount, 125 | size, 126 | memorySize 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/spring2go/okcache/store/BigCacheStore.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache.store; 2 | 3 | import com.spring2go.bigcache.BigCache; 4 | import com.spring2go.okcache.*; 5 | 6 | import java.io.IOException; 7 | import java.io.Serializable; 8 | 9 | /** 10 | * Created on Jul, 2020 by @author bobo 11 | */ 12 | public class BigCacheStore implements CacheStore { 13 | private BigCache cache; 14 | private Serializer serializer = new JavaSerializer(); 15 | 16 | public BigCacheStore(BigCache cache) { 17 | this.cache = cache; 18 | } 19 | 20 | @Override 21 | public void store(CacheEntry entry) throws Exception { 22 | if (entry == null) { 23 | return; 24 | } 25 | 26 | K key = entry.getKey(); 27 | if (key == null) { 28 | return; 29 | } 30 | 31 | byte[] value= serializer.serialize(entry); 32 | if (value == null || value.length == 0) { 33 | return; 34 | } 35 | 36 | long ttl = CacheEntryHelper.calculateTTL(entry, TimeHelper.nowMs()); 37 | cache.put(key, value, ttl); 38 | } 39 | 40 | @Override 41 | public CacheEntry load(K key) throws Exception { 42 | return remove(key); 43 | } 44 | 45 | @Override 46 | public CacheEntry remove(K key) throws Exception { 47 | if (key == null) { 48 | return null; 49 | } 50 | 51 | byte[] value = cache.delete(key); 52 | if (value == null) { 53 | return null; 54 | } 55 | 56 | return serializer.deserialize(value, CacheEntry.class); 57 | 58 | } 59 | 60 | @Override 61 | public void close() { 62 | try { 63 | cache.close(); 64 | } catch (IOException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/BigCacheLimitTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Date; 6 | 7 | import com.spring2go.bigcache.CacheConfig.StorageMode; 8 | import com.spring2go.bigcache.utils.FileUtil; 9 | import com.spring2go.bigcache.utils.TestUtil; 10 | 11 | /** 12 | * Created on Jul, 2020 by @author bobo 13 | */ 14 | public class BigCacheLimitTest { 15 | private static final String TEST_DIR = TestUtil.TEST_BASE_DIR + "stress/bigcache/"; 16 | 17 | private static BigCache cache; 18 | 19 | public static void main(String args[]) throws IOException { 20 | CacheConfig config = new CacheConfig() 21 | .setStorageMode(StorageMode.OffHeapPlusFile) 22 | .setPurgeInterval(2 * 1000) 23 | .setMergeInterval(2 * 1000) 24 | .setMaxOffHeapMemorySize(10 * 1000 * 1024 * 1024L); 25 | 26 | cache = new BigCache(TEST_DIR, config); 27 | 28 | String rndString = TestUtil.randomString(10); 29 | 30 | System.out.println("Start from date " + new Date()); 31 | long start = System.currentTimeMillis(); 32 | for (long counter = 0;; counter++) { 33 | cache.put(Long.toString(counter), rndString.getBytes()); 34 | if (counter % 1000000 == 0) { 35 | System.out.println("Current date " + new Date()); 36 | System.out.println("" + counter); 37 | System.out.println(TestUtil.printMemoryFootprint()); 38 | long end = System.currentTimeMillis(); 39 | System.out.println("timeSpent = " + (end - start)); 40 | try { 41 | Thread.sleep(5000); 42 | } catch (InterruptedException e) { 43 | } 44 | start = System.currentTimeMillis(); 45 | } 46 | } 47 | } 48 | 49 | public static void close() throws IOException { 50 | if (cache == null) 51 | return; 52 | try { 53 | cache.close(); 54 | FileUtil.deleteDirectory(new File(TEST_DIR)); 55 | } catch (IllegalStateException e) { 56 | System.gc(); 57 | try { 58 | FileUtil.deleteDirectory(new File(TEST_DIR)); 59 | } catch (IllegalStateException e1) { 60 | try { 61 | Thread.sleep(3000); 62 | } catch (InterruptedException e2) { 63 | } 64 | FileUtil.deleteDirectory(new File(TEST_DIR)); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/BigCachePerfTestA.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | import java.util.List; 11 | import java.util.concurrent.ExecutionException; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.Future; 15 | 16 | import org.junit.After; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.junit.runners.Parameterized; 20 | import org.junit.runners.Parameterized.Parameter; 21 | import org.junit.runners.Parameterized.Parameters; 22 | 23 | import com.spring2go.bigcache.CacheConfig.StorageMode; 24 | import com.spring2go.bigcache.utils.FileUtil; 25 | import com.spring2go.bigcache.utils.TestSample; 26 | import com.spring2go.bigcache.utils.TestUtil; 27 | 28 | /** 29 | * Created on Jul, 2020 by @author bobo 30 | */ 31 | @RunWith(Parameterized.class) 32 | public class BigCachePerfTestA { 33 | private static final int THREAD_COUNT = 128; 34 | private static final String TEST_DIR = TestUtil.TEST_BASE_DIR + "performance/bigcache/"; 35 | 36 | private static BigCache cache; 37 | 38 | @Parameter(value = 0) 39 | public StorageMode storageMode; 40 | 41 | @Parameters 42 | public static Collection data() throws IOException { 43 | StorageMode[][] data = { { StorageMode.PureFile }, 44 | { StorageMode.MemoryMappedPlusFile }, 45 | { StorageMode.OffHeapPlusFile } }; 46 | return Arrays.asList(data); 47 | } 48 | 49 | private BigCache cache() throws IOException { 50 | CacheConfig config = new CacheConfig(); 51 | config.setStorageMode(storageMode) 52 | .setCapacityPerBlock(20 * 1024 * 1024) 53 | .setMergeInterval(2 * 1000) 54 | .setPurgeInterval(2 * 1000); 55 | BigCache cache = new BigCache(TEST_DIR, config); 56 | return cache; 57 | } 58 | 59 | @Test 60 | public void testSingleThreadReadWrite() throws IOException, ClassNotFoundException { 61 | final int count = 400 * 1000; 62 | final TestSample sample = new TestSample(); 63 | 64 | cache = cache(); 65 | 66 | long start = System.nanoTime(); 67 | 68 | StringBuilder user = new StringBuilder(); 69 | for (int i = 0; i < count; i++) { 70 | sample.intA = i; 71 | sample.doubleA = i; 72 | sample.longA = i; 73 | cache.put(TestSample.users(user, i), sample.toBytes()); 74 | } 75 | for (int i = 0; i < count; i++) { 76 | byte[] result = cache.get(TestSample.users(user, i)); 77 | assertNotNull(result); 78 | } 79 | for (int i = 0; i < count; i++) { 80 | byte[] result = cache.get(TestSample.users(user, i)); 81 | TestSample re = TestSample.fromBytes(result); 82 | assertEquals(i, re.intA); 83 | assertEquals(i, re.doubleA, 0.0); 84 | assertEquals(i, re.longA); 85 | } 86 | for (int i = 0; i < count; i++) { 87 | cache.delete(TestSample.users(user, i)); 88 | } 89 | assertEquals(cache.count(), 0); 90 | long duration = System.nanoTime() - start; 91 | System.out.printf("Put/get %,d K operations per second%n", 92 | (int) (count * 4 * 1e6 / duration)); 93 | } 94 | 95 | @Test 96 | public void testMultiThreadReadWrite() throws InterruptedException, ExecutionException, IOException { 97 | final int count = 2 * 1000 * 1000; 98 | ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); 99 | 100 | cache = cache(); 101 | 102 | long start = System.nanoTime(); 103 | List> futures = new ArrayList>(); 104 | for (int i = 0; i < THREAD_COUNT; i++) { 105 | final int finalI = i; 106 | futures.add(service.submit(new Runnable() { 107 | 108 | @Override 109 | public void run() { 110 | try { 111 | final TestSample sample = new TestSample(); 112 | StringBuilder user = new StringBuilder(); 113 | for (int j = finalI; j < count; j += THREAD_COUNT) { 114 | sample.intA = j; 115 | sample.doubleA = j; 116 | sample.longA = j; 117 | cache.put(TestSample.users(user, j), sample.toBytes()); 118 | } 119 | for (int j = finalI; j < count; j += THREAD_COUNT) { 120 | byte[] result = cache.get(TestSample.users(user, j)); 121 | assertNotNull(result); 122 | } 123 | for (int j = finalI; j < count; j += THREAD_COUNT) { 124 | byte[] result = cache.get(TestSample.users(user, j)); 125 | TestSample re = TestSample.fromBytes(result); 126 | assertEquals(j, re.intA); 127 | assertEquals(j, re.doubleA, 0.0); 128 | assertEquals(j, re.longA); 129 | } 130 | for (int j = finalI; j < count; j += THREAD_COUNT) { 131 | cache.delete(TestSample.users(user, j)); 132 | } 133 | } catch (IOException e) { 134 | e.printStackTrace(); 135 | } catch (ClassNotFoundException e1) { 136 | e1.printStackTrace(); 137 | } 138 | } 139 | 140 | })); 141 | } 142 | 143 | for (Future future : futures) { 144 | future.get(); 145 | } 146 | 147 | long duration = System.nanoTime() - start; 148 | System.out.printf("Put/get %,d K operations per second%n", 149 | (int) (count * 4 * 1e6 / duration)); 150 | service.shutdown(); 151 | } 152 | 153 | @After 154 | public void close() throws IOException { 155 | try { 156 | cache.close(); 157 | FileUtil.deleteDirectory(new File(TEST_DIR)); 158 | } catch (IllegalStateException e) { 159 | System.gc(); 160 | try { 161 | FileUtil.deleteDirectory(new File(TEST_DIR)); 162 | } catch (IllegalStateException e1) { 163 | try { 164 | Thread.sleep(3000); 165 | } catch (InterruptedException e2) { 166 | } 167 | FileUtil.deleteDirectory(new File(TEST_DIR)); 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/BigCacheReadWriteStressTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | import java.util.List; 11 | import java.util.concurrent.ExecutionException; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.Future; 15 | 16 | import org.junit.After; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.junit.runners.Parameterized; 20 | import org.junit.runners.Parameterized.Parameter; 21 | import org.junit.runners.Parameterized.Parameters; 22 | 23 | import com.spring2go.bigcache.CacheConfig.StorageMode; 24 | import com.spring2go.bigcache.utils.FileUtil; 25 | import com.spring2go.bigcache.utils.TestSample; 26 | import com.spring2go.bigcache.utils.TestUtil; 27 | 28 | /** 29 | * Created on Jul, 2020 by @author bobo 30 | */ 31 | @RunWith(Parameterized.class) 32 | public class BigCacheReadWriteStressTest { 33 | private static final String TEST_DIR = TestUtil.TEST_BASE_DIR + "stress/bigcache/"; 34 | 35 | private static BigCache cache; 36 | 37 | @Parameter(value = 0) 38 | public StorageMode storageMode; 39 | 40 | @Parameters 41 | public static Collection data() throws IOException { 42 | StorageMode[][] data = { //{ StorageMode.PureFile }, 43 | { StorageMode.MemoryMappedPlusFile }, 44 | { StorageMode.OffHeapPlusFile } }; 45 | return Arrays.asList(data); 46 | } 47 | 48 | public BigCache cache(long count) throws IOException { 49 | CacheConfig config = new CacheConfig(); 50 | config.setStorageMode(storageMode); 51 | BigCache cache = new BigCache(TEST_DIR, config); 52 | 53 | for (long i = 0; i < count; i++) { 54 | String key = "" + i; 55 | cache.put(key, key.getBytes()); 56 | } 57 | return cache; 58 | } 59 | 60 | @Test 61 | public void testWrite_ten_million() throws IOException { 62 | final long item_count = 10 * 1000 * 1000; 63 | long elapsedTime = 0; 64 | long count = 0; 65 | cache = cache(0); 66 | long startTime = System.nanoTime(); 67 | 68 | for (long i = 0; i < item_count; i++) { 69 | String key = "" + i; 70 | long start = System.nanoTime(); 71 | cache.put(key, key.getBytes()); 72 | 73 | if (i % (100 * 1000) == 0) { 74 | elapsedTime += (System.nanoTime() - start); 75 | count++; 76 | } 77 | } 78 | 79 | System.out.println("avg:" + elapsedTime / count + " ns"); 80 | System.out.println("write " + item_count / (1000 * 1000) + " million times:" 81 | + (System.nanoTime() - startTime) / (1000 * 1000) + " ms"); 82 | } 83 | 84 | @Test 85 | public void testRead_ten_million() throws IOException { 86 | final long item_count = 10 * 1000 * 1000; 87 | cache = cache(item_count); 88 | long startTime = System.nanoTime(); 89 | 90 | for (long i = 0; i < item_count; i++) { 91 | String key = "" + i; 92 | cache.get(key); 93 | } 94 | System.out.println("read " + item_count / (1000 * 1000) + " million times:" + (System.nanoTime() - startTime) 95 | / (1000 * 1000) 96 | + " ms"); 97 | } 98 | 99 | @Test 100 | public void testReadWrite_one_million() throws IOException { 101 | final long item_count = 1000 * 1000; 102 | final int keyLen = 8; 103 | final int valueLen = 128; 104 | this.executeReadWrite(item_count, keyLen, valueLen); 105 | } 106 | 107 | @Test 108 | public void testReadWrite_two_million() throws IOException { 109 | final long item_count = 2 * 1000 * 1000; 110 | final int keyLen = 8; 111 | final int valueLen = 1024; 112 | this.executeReadWrite(item_count, keyLen, valueLen); 113 | } 114 | 115 | private void executeReadWrite(final long count, int keyLen, int valueLen) throws IOException { 116 | final int defaultKeyLen = 8; 117 | final int defaultValueLen = 32; 118 | 119 | CacheConfig config = new CacheConfig(); 120 | config.setStorageMode(storageMode); 121 | cache = new BigCache(TEST_DIR, config); 122 | List keys = new ArrayList(); 123 | 124 | String key = ""; 125 | String value = ""; 126 | if (keyLen > 0) { 127 | key = TestUtil.randomString(keyLen); 128 | } else { 129 | key = TestUtil.randomString(defaultKeyLen); 130 | } 131 | if (valueLen > 0) { 132 | value = TestUtil.randomString(valueLen); 133 | } else { 134 | value = TestUtil.randomString(defaultValueLen); 135 | } 136 | for (int i = 0; i < count; i++) { 137 | cache.put(key + i, TestUtil.getBytes(value + i)); 138 | keys.add(key + i); 139 | } 140 | assertEquals(cache.count(), count); 141 | for (String k : keys) { 142 | String v = new String(cache.get(k)); 143 | String index = k.substring(keyLen > 0 ? keyLen : 8); 144 | assertEquals(value + index, v); 145 | } 146 | for (String k : keys) { 147 | cache.delete(k); 148 | } 149 | for (String k : keys) { 150 | assertNull(cache.get(k)); 151 | } 152 | } 153 | 154 | @Test 155 | public void testMultiThreadWriteTtl_two_million() throws InterruptedException, ExecutionException, IOException { 156 | final int count = 2 * 1000 * 1000; 157 | final int threadCount = 16; 158 | ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); 159 | 160 | CacheConfig config = new CacheConfig(); 161 | config.setStorageMode(storageMode) 162 | .setCapacityPerBlock(20 * 1024 * 1024) 163 | .setMergeInterval(2 * 1000) 164 | .setPurgeInterval(2 * 1000); 165 | cache = new BigCache(TEST_DIR, config); 166 | 167 | long start = System.nanoTime(); 168 | List> futures = new ArrayList>(); 169 | for (int i = 0; i < threadCount; i++) { 170 | final int finalI = i; 171 | futures.add(service.submit(new Runnable() { 172 | 173 | @Override 174 | public void run() { 175 | try { 176 | final TestSample sample = new TestSample(); 177 | StringBuilder user = new StringBuilder(); 178 | for (int j = finalI; j < count; j += threadCount) { 179 | sample.intA = j; 180 | sample.doubleA = j; 181 | sample.longA = j; 182 | cache.put(TestSample.users(user, j), sample.toBytes(), 2 * 1000); 183 | } 184 | Thread.sleep(10 * 1000); 185 | for (int j = finalI; j < count; j += threadCount) { 186 | assertNull(cache.get(TestSample.users(user, j))); 187 | } 188 | } catch (IOException e) { 189 | e.printStackTrace(); 190 | } catch (InterruptedException e) { 191 | e.printStackTrace(); 192 | } 193 | } 194 | 195 | })); 196 | } 197 | 198 | for (Future future : futures) { 199 | future.get(); 200 | } 201 | 202 | long duration = System.nanoTime() - start; 203 | System.out.printf("Put/get %,d K operations per second%n", 204 | (int) (count * 4 * 1e6 / duration)); 205 | service.shutdown(); 206 | } 207 | 208 | @After 209 | public void close() throws IOException { 210 | try { 211 | cache.close(); 212 | FileUtil.deleteDirectory(new File(TEST_DIR)); 213 | } catch (IllegalStateException e) { 214 | System.gc(); 215 | try { 216 | FileUtil.deleteDirectory(new File(TEST_DIR)); 217 | } catch (IllegalStateException e1) { 218 | try { 219 | Thread.sleep(10000); 220 | } catch (InterruptedException e2) { 221 | } 222 | FileUtil.deleteDirectory(new File(TEST_DIR)); 223 | } 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/BigCacheStressTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import java.util.Date; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Random; 7 | 8 | import com.spring2go.bigcache.CacheConfig.StorageMode; 9 | import com.spring2go.bigcache.utils.TestUtil; 10 | 11 | /** 12 | * Created on Jul, 2020 by @author bobo 13 | */ 14 | public class BigCacheStressTest { 15 | private static final String TEST_DIR = TestUtil.TEST_BASE_DIR + "stress/bigcache/"; 16 | 17 | private static BigCache cache; 18 | 19 | public static void main(String[] args) throws Exception { 20 | int numKeyLimit = 1024 * 16; 21 | int valueLengthLimit = 1024 * 16; 22 | 23 | CacheConfig config = new CacheConfig(); 24 | config.setStorageMode(StorageMode.OffHeapPlusFile) 25 | .setPurgeInterval(2 * 1000) 26 | .setMergeInterval(2 * 1000) 27 | //allocate half of the memory - leave space for keys and reference map. 28 | .setMaxOffHeapMemorySize(5 * 1000 * 1024 * 1024L); 29 | cache = new BigCache(TEST_DIR, config); 30 | Map map = new HashMap(); 31 | 32 | String[] rndStrings = { TestUtil.randomString(valueLengthLimit / 2), 33 | TestUtil.randomString(valueLengthLimit), 34 | TestUtil.randomString(valueLengthLimit + valueLengthLimit / 2) }; 35 | byte[] rndBytes = rndStrings[1].getBytes(); 36 | 37 | Random random = new Random(); 38 | 39 | System.out.println("Start from date " + new Date()); 40 | long start = System.currentTimeMillis(); 41 | for (long counter = 0;; counter++) { 42 | int rndKey = random.nextInt(numKeyLimit); 43 | boolean put = random.nextDouble() < 0.5 ? true : false; 44 | if (put) { 45 | rndBytes = rndStrings[random.nextInt(3)].getBytes(); 46 | map.put(String.valueOf(rndKey), rndBytes); 47 | cache.put(String.valueOf(rndKey), rndBytes); 48 | } else { 49 | map.remove(String.valueOf(rndKey)); 50 | byte[] oldV = cache.delete(String.valueOf(rndKey)); 51 | byte[] v = cache.get(String.valueOf(rndKey)); 52 | if (v != null) { 53 | System.out.println("should be null. Key:" + String.valueOf(rndKey) + " Value:" + new String(v)); 54 | System.out.println(" Key:" + String.valueOf(rndKey) + " oldValue:" 55 | + (oldV == null ? null : new String(oldV))); 56 | } 57 | } 58 | 59 | cache.put(counter + "-ttl", rndBytes, (long) 10 * 1000); 60 | 61 | if (counter % 1000000 == 0) { 62 | System.out.println("Current date: " + new Date()); 63 | System.out.println("counter: " + counter); 64 | System.out.println("purge: " + cache.getStats().getCacheExpire()); 65 | System.out.println("move: " + cache.getStats().getCacheMove()); 66 | System.out.println("size: " + cache.count()); 67 | long cacheUsed = cache.getStats().getStorageUsed(); 68 | System.out.println("used: " + cacheUsed); 69 | System.out.println(); 70 | 71 | long storeUsed = cache.storageManager.getUsed(); 72 | if (cacheUsed != storeUsed) { 73 | System.out.println("!!!! Temporarily fail the test, this could seldom occur"); 74 | System.out.println("storage used: " + storeUsed + ", but cache used: " + cacheUsed); 75 | } 76 | 77 | System.out.println(); 78 | System.out.println(TestUtil.printMemoryFootprint()); 79 | long end = System.currentTimeMillis(); 80 | System.out.println("timeSpent = " + (end - start)); 81 | System.out.println("ttl count = " + (cache.count() - map.size())); 82 | System.out.println("used size = " + cache.getStats().getStorageUsed()); 83 | 84 | // validation 85 | for (int i = 0; i < numKeyLimit; i++) { 86 | String key = String.valueOf(i); 87 | byte[] mapValue = map.get(key); 88 | byte[] cacheValue = cache.get(key); 89 | 90 | if (mapValue == null && cacheValue != null) { 91 | System.out.println("Key:" + key); 92 | System.out.println("Value:" + new String(cacheValue)); 93 | throw new RuntimeException("Validation exception, key exists in cache but not in map"); 94 | } 95 | if (mapValue != null && cacheValue == null) { 96 | throw new RuntimeException("Validation exception, key exists in map but not in cache"); 97 | } 98 | if (mapValue != null && cacheValue != null) { 99 | if (compare(mapValue, cacheValue) != 0) { 100 | throw new RuntimeException("Validation exception, values in map and cache does not equal"); 101 | } 102 | } 103 | } 104 | 105 | 106 | //exclude validation process from time count 107 | start = System.currentTimeMillis(); 108 | } 109 | } 110 | } 111 | 112 | public static int compare(byte[] left, byte[] right) { 113 | for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) { 114 | int a = (left[i] & 0xff); 115 | int b = (right[j] & 0xff); 116 | if (a != b) { 117 | return a - b; 118 | } 119 | } 120 | return left.length - right.length; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/BigCacheTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | import static org.junit.Assert.assertNull; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | 14 | import org.junit.After; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.junit.runners.Parameterized; 18 | import org.junit.runners.Parameterized.Parameter; 19 | import org.junit.runners.Parameterized.Parameters; 20 | 21 | import com.spring2go.bigcache.CacheConfig.StorageMode; 22 | import com.spring2go.bigcache.utils.FileUtil; 23 | import com.spring2go.bigcache.utils.TestUtil; 24 | 25 | /** 26 | * Created on Jul, 2020 by @author bobo 27 | */ 28 | @RunWith(Parameterized.class) 29 | public class BigCacheTest { 30 | private static final double STRESS_FACTOR = Double.parseDouble(System.getProperty("STRESS_FACTOR", "1.0")); 31 | private static final String TEST_DIR = TestUtil.TEST_BASE_DIR + "function/bigcache/"; 32 | 33 | private BigCache cache; 34 | 35 | @Parameter(value = 0) 36 | public StorageMode storageMode; 37 | 38 | @Parameters 39 | public static Collection data() throws IOException { 40 | StorageMode[][] data = { { StorageMode.PureFile }, 41 | { StorageMode.MemoryMappedPlusFile }, 42 | { StorageMode.OffHeapPlusFile } }; 43 | return Arrays.asList(data); 44 | } 45 | 46 | @Test 47 | public void testBigCache() throws IOException { 48 | CacheConfig config = new CacheConfig(); 49 | config.setStorageMode(storageMode); 50 | cache = new BigCache(TEST_DIR, config); 51 | Set rndStringSet = new HashSet(); 52 | for (int i = 0; i < 2000000 * STRESS_FACTOR; i++) { 53 | String rndString = TestUtil.randomString(64); 54 | rndStringSet.add(rndString); 55 | cache.put(rndString, rndString.getBytes()); 56 | if ((i % 50000) == 0 && i != 0) { 57 | System.out.println(i + " rows written"); 58 | } 59 | } 60 | 61 | for (String rndString : rndStringSet) { 62 | byte[] value = cache.get(rndString); 63 | assertNotNull(value); 64 | assertEquals(rndString, new String(value)); 65 | } 66 | 67 | // delete 68 | for (String rndString : rndStringSet) { 69 | cache.delete(rndString); 70 | } 71 | 72 | for (String rndString : rndStringSet) { 73 | byte[] value = cache.get(rndString); 74 | assertNull(value); 75 | } 76 | } 77 | 78 | // @Test(expected = IllegalArgumentException.class) 79 | // public void testInvalidFileDir() { 80 | // String fakeDir = "dltestDB://bigcache_test/asdl"; 81 | // CacheConfig config = new CacheConfig(); 82 | // try { 83 | // cache = new BigCache(fakeDir, config); 84 | // } catch (IOException e) { 85 | // } 86 | // } 87 | 88 | @Test(expected = IllegalArgumentException.class) 89 | public void testInvalidCacheConfig() { 90 | CacheConfig config = new CacheConfig().setCapacityPerBlock(12) 91 | .setConcurrencyLevel(2345) 92 | .setInitialNumberOfBlocks(27); 93 | } 94 | 95 | @After 96 | public void close() throws IOException { 97 | if (cache == null) 98 | return; 99 | try { 100 | cache.close(); 101 | FileUtil.deleteDirectory(new File(TEST_DIR)); 102 | } catch (IllegalStateException e) { 103 | System.gc(); 104 | try { 105 | FileUtil.deleteDirectory(new File(TEST_DIR)); 106 | } catch (IllegalStateException e1) { 107 | try { 108 | Thread.sleep(3000); 109 | } catch (InterruptedException e2) { 110 | } 111 | FileUtil.deleteDirectory(new File(TEST_DIR)); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/BigCacheUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | 10 | import org.junit.After; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.junit.runners.Parameterized; 14 | import org.junit.runners.Parameterized.Parameter; 15 | import org.junit.runners.Parameterized.Parameters; 16 | 17 | import com.spring2go.bigcache.CacheConfig.StorageMode; 18 | import com.spring2go.bigcache.utils.FileUtil; 19 | import com.spring2go.bigcache.utils.TestUtil; 20 | 21 | /** 22 | * Created on Jul, 2020 by @author bobo 23 | */ 24 | @RunWith(Parameterized.class) 25 | public class BigCacheUnitTest { 26 | private static String TEST_DIR = TestUtil.TEST_BASE_DIR + "unit/bigcache/"; 27 | private static BigCache cache; 28 | 29 | @Parameter(value = 0) 30 | public StorageMode storageMode; 31 | 32 | @Parameters 33 | public static Collection data() throws IOException { 34 | StorageMode[][] data = { { StorageMode.PureFile }, 35 | { StorageMode.MemoryMappedPlusFile }, 36 | { StorageMode.OffHeapPlusFile } }; 37 | return Arrays.asList(data); 38 | } 39 | 40 | public BigCache cache6() throws IOException { 41 | CacheConfig config = new CacheConfig(); 42 | config.setStorageMode(storageMode); 43 | BigCache cache = new BigCache(TEST_DIR, config); 44 | cache.put(0, "A".getBytes()); 45 | cache.put(1, "B".getBytes()); 46 | cache.put(2, "C".getBytes()); 47 | cache.put(3, "D".getBytes()); 48 | cache.put(4, "E".getBytes()); 49 | cache.put(5, "F".getBytes()); 50 | return cache; 51 | } 52 | 53 | @Test 54 | public void testGet() throws IOException { 55 | cache = cache6(); 56 | assertEquals(new String(cache.get(0)), "A"); 57 | assertEquals(new String(cache.get(1)), "B"); 58 | assertEquals(new String(cache.get(2)), "C"); 59 | assertEquals(new String(cache.get(3)), "D"); 60 | assertEquals(new String(cache.get(4)), "E"); 61 | assertEquals(new String(cache.get(5)), "F"); 62 | } 63 | 64 | @Test 65 | public void testPut() throws IOException { 66 | cache = cache6(); 67 | //test put new 68 | cache.put(6, "G".getBytes()); 69 | assertEquals(new String(cache.get(6)), "G"); 70 | //test replace old 71 | cache.put(0, "W".getBytes()); 72 | assertEquals(new String(cache.get(0)), "W"); 73 | } 74 | 75 | @Test 76 | public void testDelete() throws IOException { 77 | cache = cache6(); 78 | byte[] old = cache.delete(0); 79 | assertEquals(new String(old), "A"); 80 | assertNull(cache.get(0)); 81 | 82 | old = cache.delete(6); 83 | assertNull(old); 84 | assertNull(cache.get(6)); 85 | } 86 | 87 | @Test 88 | public void testContain() throws IOException { 89 | cache = cache6(); 90 | assertTrue(cache.contains(0)); 91 | assertFalse(cache.contains(6)); 92 | } 93 | 94 | @After 95 | public void close() throws IOException { 96 | try { 97 | cache.close(); 98 | FileUtil.deleteDirectory(new File(TEST_DIR)); 99 | } catch (IllegalStateException e) { 100 | System.gc(); 101 | try { 102 | FileUtil.deleteDirectory(new File(TEST_DIR)); 103 | } catch (IllegalStateException e1) { 104 | try { 105 | Thread.sleep(3000); 106 | } catch (InterruptedException e2) { 107 | } 108 | FileUtil.deleteDirectory(new File(TEST_DIR)); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/storage/FileChannelStorageTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import com.spring2go.bigcache.utils.FileUtil; 4 | import com.spring2go.bigcache.utils.TestUtil; 5 | import org.junit.After; 6 | import org.junit.Test; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | /** 14 | * Created on Jul, 2020 by @author bobo 15 | */ 16 | public class FileChannelStorageTest { 17 | private static String testDir = TestUtil.TEST_BASE_DIR + "unit/file_channel_storage_test/"; 18 | 19 | private IStorage fileChannelStorage = null; 20 | 21 | @Test 22 | public void testFileChannelStorage() throws IOException { 23 | fileChannelStorage = new FileChannelStorage(testDir, 1, 16 * 1024 * 1024); 24 | 25 | // test one item 26 | String testString = "Test String"; 27 | fileChannelStorage.put(0, testString.getBytes()); 28 | byte[] dest = new byte[testString.getBytes().length]; 29 | fileChannelStorage.get(0, dest); 30 | 31 | assertEquals(testString, new String(dest)); 32 | 33 | // test limit items 34 | int limit = 1000; 35 | int[] positionArray = new int[limit]; 36 | int[] lengthArray = new int[limit]; 37 | int position = 0; 38 | for(int i = 0; i < limit; i++) { 39 | byte[] src = (testString + i).getBytes(); 40 | positionArray[i] = position; 41 | lengthArray[i] = src.length; 42 | fileChannelStorage.put(position, src); 43 | position += src.length; 44 | } 45 | 46 | for(int i = 0; i < limit; i++) { 47 | dest = new byte[lengthArray[i]]; 48 | fileChannelStorage.get(positionArray[i], dest); 49 | assertEquals(testString + i, new String(dest)); 50 | } 51 | } 52 | 53 | @After 54 | public void clear() throws IOException { 55 | if (this.fileChannelStorage != null) { 56 | this.fileChannelStorage.close(); 57 | } 58 | FileUtil.deleteDirectory(new File(testDir)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/storage/StorageUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.storage; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | 10 | import org.junit.After; 11 | import org.junit.AfterClass; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.junit.runners.Parameterized; 15 | import org.junit.runners.Parameterized.Parameter; 16 | import org.junit.runners.Parameterized.Parameters; 17 | 18 | import com.spring2go.bigcache.utils.FileUtil; 19 | import com.spring2go.bigcache.utils.TestUtil; 20 | 21 | /** 22 | * Created on Jul, 2020 by @author bobo 23 | */ 24 | @RunWith(Parameterized.class) 25 | public class StorageUnitTest { 26 | private static final String TEST_DIR = TestUtil.TEST_BASE_DIR + "unit/istorage_test/"; 27 | private static IStorage[][] storageData; 28 | 29 | @Parameter(value = 0) 30 | public IStorage storage; 31 | 32 | @Parameters 33 | public static Collection data() throws IOException { 34 | storageData = new IStorage[][] { { new FileChannelStorage(TEST_DIR, 0, 16 * 1024 * 1024) }, 35 | { new MemoryMappedStorage(TEST_DIR, 0, 16 * 1024 * 1024) }, 36 | { new OffHeapStorage(16 * 1024 * 1024) } }; 37 | return Arrays.asList(storageData); 38 | } 39 | 40 | public void storage6() throws IOException { 41 | storage.put(0, "A".getBytes()); 42 | storage.put(1, "B".getBytes()); 43 | storage.put(2, "C".getBytes()); 44 | storage.put(3, "D".getBytes()); 45 | storage.put(4, "E".getBytes()); 46 | storage.put(5, "F".getBytes()); 47 | } 48 | 49 | @Test 50 | public void testGet() throws IOException { 51 | storage6(); 52 | byte[] dest = new byte["A".getBytes().length]; 53 | storage.get(0, dest); 54 | assertEquals(new String(dest), "A"); 55 | storage.get(1, dest); 56 | assertEquals(new String(dest), "B"); 57 | storage.get(2, dest); 58 | assertEquals(new String(dest), "C"); 59 | storage.get(3, dest); 60 | assertEquals(new String(dest), "D"); 61 | storage.get(4, dest); 62 | assertEquals(new String(dest), "E"); 63 | storage.get(5, dest); 64 | assertEquals(new String(dest), "F"); 65 | } 66 | 67 | @Test 68 | public void testPut() throws IOException { 69 | storage6(); 70 | //test put new 71 | storage.put(6, "G".getBytes()); 72 | byte[] dest = new byte["A".getBytes().length]; 73 | storage.get(6, dest); 74 | assertEquals(new String(dest), "G"); 75 | //test replace old 76 | storage.put(0, "W".getBytes()); 77 | storage.get(0, dest); 78 | assertEquals(new String(dest), "W"); 79 | } 80 | 81 | @After 82 | public void clear() throws IOException { 83 | storage.free(); 84 | } 85 | 86 | @AfterClass 87 | public static void tearDown() throws IOException { 88 | for (IStorage[] storage : storageData) { 89 | storage[0].close(); 90 | } 91 | try { 92 | FileUtil.deleteDirectory(new File(TEST_DIR)); 93 | } catch (IllegalStateException e) { 94 | System.gc(); 95 | try { 96 | FileUtil.deleteDirectory(new File(TEST_DIR)); 97 | } catch (IllegalStateException e1) { 98 | try { 99 | Thread.sleep(3000); 100 | } catch (InterruptedException e2) { 101 | } 102 | FileUtil.deleteDirectory(new File(TEST_DIR)); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/utils/TestSample.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.utils; 2 | 3 | import java.io.*; 4 | 5 | /** 6 | * Created on Jul, 2020 by @author bobo 7 | */ 8 | public class TestSample implements Serializable { 9 | private static final long serialVersionUID = 1L; 10 | public String stringA = "aaaaaaaaaa"; 11 | public String stringB = "bbbbbbbbbb"; 12 | public BuySell enumA = BuySell.Buy; 13 | public BuySell enumB = BuySell.Sell; 14 | public int intA = 123456; 15 | public int intB = 654321; 16 | public double doubleA = 1.23456789; 17 | public double doubleB = 9.87654321; 18 | public long longA = 987654321; 19 | public long longB = 123456789; 20 | 21 | public byte[] toBytes() throws IOException { 22 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 23 | ObjectOutput out = null; 24 | try { 25 | out = new ObjectOutputStream(bos); 26 | out.writeObject(this); 27 | byte[] yourBytes = bos.toByteArray(); 28 | return yourBytes; 29 | } finally { 30 | try { 31 | if (out != null) { 32 | out.close(); 33 | } 34 | } catch (IOException ex) { 35 | // ignore close exception 36 | } 37 | try { 38 | bos.close(); 39 | } catch (IOException ex) { 40 | // ignore close exception 41 | } 42 | } 43 | } 44 | 45 | public static TestSample fromBytes(byte[] bytes) throws ClassNotFoundException, IOException { 46 | ByteArrayInputStream bis = new ByteArrayInputStream(bytes); 47 | ObjectInput in = null; 48 | try { 49 | in = new ObjectInputStream(bis); 50 | Object o = in.readObject(); 51 | return (TestSample) o; 52 | } finally { 53 | try { 54 | bis.close(); 55 | } catch (IOException ex) { 56 | // ignore close exception 57 | } 58 | try { 59 | if (in != null) { 60 | in.close(); 61 | } 62 | } catch (IOException ex) { 63 | // ignore close exception 64 | } 65 | } 66 | } 67 | 68 | enum BuySell { 69 | Buy, Sell 70 | } 71 | 72 | public static String users(StringBuilder user, int i) { 73 | user.setLength(0); 74 | user.append("user:"); 75 | user.append(i); 76 | return user.toString(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/bigcache/utils/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.bigcache.utils; 2 | 3 | import java.io.*; 4 | import java.text.DecimalFormat; 5 | import java.text.NumberFormat; 6 | import java.util.Random; 7 | 8 | /** 9 | * Created on Jul, 2020 by @author bobo 10 | */ 11 | public class TestUtil { 12 | static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 13 | static Random rnd = new Random(); 14 | 15 | private static final NumberFormat MEM_FMT = new DecimalFormat("##,###.##"); 16 | 17 | public static String randomString(int len ) 18 | { 19 | StringBuilder sb = new StringBuilder( len ); 20 | for( int i = 0; i < len; i++ ) 21 | sb.append( AB.charAt( rnd.nextInt(AB.length()) ) ); 22 | return sb.toString(); 23 | } 24 | 25 | public static void sleepQuietly(long duration) { 26 | try { 27 | Thread.sleep(duration); 28 | } catch (InterruptedException e) { 29 | // ignore 30 | } 31 | } 32 | 33 | public static final String TEST_BASE_DIR = "/Users/william/test/bigcache_test/"; 34 | 35 | public static String kbString(long memBytes) { 36 | return MEM_FMT.format(memBytes / 1024) + " kb"; 37 | } 38 | 39 | public static String printMemoryFootprint() { 40 | Runtime run = Runtime.getRuntime(); 41 | String memoryInfo = "Memory - free: " + kbString(run.freeMemory()) + " - max:" + kbString(run.maxMemory()) + "- total:" + kbString(run.totalMemory()); 42 | return memoryInfo; 43 | } 44 | 45 | public static byte[] getBytes(Object o) { 46 | if (o instanceof String) { 47 | return ((String)o).getBytes(); 48 | } else if (o instanceof byte[]) { 49 | return (byte[])o; 50 | } else if (o instanceof Serializable) { 51 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 52 | ObjectOutput out = null; 53 | try { 54 | out = new ObjectOutputStream(bos); 55 | out.writeObject(o); 56 | byte[] bytes = bos.toByteArray(); 57 | return bytes; 58 | } catch(Exception e) { 59 | return null; 60 | } finally { 61 | try { 62 | if (out != null) { 63 | out.close(); 64 | } 65 | } catch (IOException ex) { 66 | // ignore close exception 67 | } 68 | try { 69 | bos.close(); 70 | } catch (IOException ex) { 71 | // ignore close exception 72 | } 73 | } 74 | } 75 | throw new RuntimeException("Fail to convert object to bytes"); 76 | } 77 | 78 | public static String convertToString(byte[] bytes){ 79 | return new String(bytes); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/lrucache/v1/LruCacheV1Test.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.lrucache.v1; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | */ 9 | public class LruCacheV1Test { 10 | 11 | @Test 12 | public void test1() { 13 | LruCacheV1 cache = new LruCacheV1<>(5); 14 | cache.put(1, 1); 15 | cache.put(2, 2); 16 | cache.put(3, 3); 17 | cache.put(4, 4); 18 | cache.put(5, 5); 19 | 20 | Assert.assertTrue(cache.get(1) == 1 && cache.get(2) == 2 && cache.get(4) == 4); 21 | } 22 | 23 | @Test 24 | public void test2() { 25 | LruCacheV1 cache = new LruCacheV1<>(5); 26 | cache.put(1, 1); 27 | cache.put(2, 2); 28 | cache.put(3, 3); 29 | cache.put(4, 4); 30 | cache.put(5, 5); 31 | 32 | cache.put(6, 6); 33 | 34 | Assert.assertEquals(cache.size(), 5); 35 | } 36 | 37 | @Test 38 | public void test3() { 39 | LruCacheV1 cache = new LruCacheV1<>(5); 40 | cache.put(1, 1); 41 | cache.put(2, 2); 42 | cache.put(3, 3); 43 | cache.put(4, 4); 44 | cache.put(5, 5); 45 | 46 | cache.put(6, 6); 47 | 48 | cache.put(7, 7); 49 | 50 | Assert.assertTrue(cache.get(4) == 4 && cache.get(6) == 6 && cache.get(7) == 7); 51 | } 52 | 53 | @Test 54 | public void test4() { 55 | LruCacheV1 cache = new LruCacheV1<>(5); 56 | cache.put(1, 1); 57 | cache.put(2, 2); 58 | cache.put(3, 3); 59 | cache.put(4, 4); 60 | cache.put(5, 5); 61 | 62 | cache.put(6, 6); 63 | cache.put(7, 7); 64 | 65 | Assert.assertTrue(cache.get(1) == null); 66 | } 67 | 68 | @Test 69 | public void test5() { 70 | LruCacheV1 cache = new LruCacheV1<>(3); 71 | cache.put(1, 1); 72 | cache.put(2, 2); 73 | cache.put(1, 1); 74 | cache.put(3, 3); 75 | cache.put(4, 4); 76 | 77 | Assert.assertTrue(cache.get(1) == 1); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/lrucache/v2/LruCacheV2PerfTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.lrucache.v2; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import com.spring2go.bigcache.utils.TestUtil; 7 | import org.junit.Test; 8 | 9 | import java.util.Queue; 10 | import java.util.concurrent.BlockingQueue; 11 | import java.util.concurrent.CountDownLatch; 12 | import java.util.concurrent.LinkedBlockingQueue; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | /** 16 | * Created on Jul, 2020 by @author bobo 17 | */ 18 | public class LruCacheV2PerfTest { 19 | /********************* configurable parameters *********************/ 20 | private static final int LOOP = 5; 21 | private static final int ITEM_COUNT = 1000000; 22 | private static final int PRODUCER_COUNT = 4; 23 | private static final int CONSUMER_COUNT = 4; 24 | private static final int STRING_LEN = 16; 25 | /******************************************************************/ 26 | 27 | private final AtomicInteger producingItemCount = new AtomicInteger(0); 28 | private final AtomicInteger consumingItemCount = new AtomicInteger(0); 29 | private final BlockingQueue keysInMemoryQueue = new LinkedBlockingQueue(); 30 | 31 | private static LruCacheV2 cache; 32 | 33 | @Test 34 | public void testProduceThenConsume() throws InterruptedException { 35 | System.out.println("LruCache performance test begin ..."); 36 | for (int i = 0; i < LOOP; i++) { 37 | cache = new LruCacheV2<>(ITEM_COUNT * 2); 38 | System.out.println("[doRunProduceThenConsume] round " + (i + 1) + " of " + LOOP); 39 | this.doRunProduceThenConsume(); 40 | producingItemCount.set(0); 41 | consumingItemCount.set(0); 42 | } 43 | System.out.println("[doRunProduceThenConsume] test ends"); 44 | } 45 | 46 | @Test 47 | public void testProduceMixedConsume() throws InterruptedException { 48 | for (int i = 0; i < LOOP; i++) { 49 | cache = new LruCacheV2<>(ITEM_COUNT * 2); 50 | System.out.println("[doRunMixed] round " + (i + 1) + " of " + LOOP); 51 | this.doRunProduceMixedConsume(); 52 | producingItemCount.set(0); 53 | consumingItemCount.set(0); 54 | } 55 | System.out.println("[doRunMixed] test ends"); 56 | } 57 | 58 | private void doRunProduceThenConsume() throws InterruptedException { 59 | CountDownLatch producerLatch = new CountDownLatch(PRODUCER_COUNT); 60 | CountDownLatch consumerLatch = new CountDownLatch(CONSUMER_COUNT); 61 | BlockingQueue producerResults = new LinkedBlockingQueue(); 62 | BlockingQueue consumerResults = new LinkedBlockingQueue(); 63 | long start = System.nanoTime(); 64 | long totalProducingTime = 0; 65 | long totalConsumingTime = 0; 66 | 67 | for (int i = 0; i < PRODUCER_COUNT; i++) { 68 | Producer p = new Producer(producerLatch, producerResults); 69 | p.start(); 70 | } 71 | 72 | for (int i = 0; i < PRODUCER_COUNT; i++) { 73 | Result result = producerResults.take(); 74 | assertEquals(result.status, Status.SUCCESS); 75 | totalProducingTime += result.duration; 76 | } 77 | long duration = System.nanoTime() - start; 78 | 79 | System.out.println("-----------------------------------------------"); 80 | System.out.println("Producing test result:"); 81 | System.out.printf("Total test time = %d ns.\n", duration); 82 | System.out.printf("Total item count = %d\n", ITEM_COUNT); 83 | System.out.printf("Producer thread number = %d\n", PRODUCER_COUNT); 84 | System.out.printf("Item message length = %d bytes\n", STRING_LEN); 85 | System.out.printf("Total producing time = %d ns.\n", totalProducingTime); 86 | System.out.printf("Average producint time = %d ns.\n", totalProducingTime / PRODUCER_COUNT); 87 | System.out.println("-----------------------------------------------"); 88 | 89 | // the consumer start 90 | start = System.nanoTime(); 91 | for (int i = 0; i < CONSUMER_COUNT; i++) { 92 | Consumer c = new Consumer(consumerLatch, consumerResults); 93 | c.start(); 94 | } 95 | 96 | for (int i = 0; i < CONSUMER_COUNT; i++) { 97 | Result result = consumerResults.take(); 98 | assertEquals(result.status, Status.SUCCESS); 99 | totalConsumingTime += result.duration; 100 | } 101 | duration = System.nanoTime() - start; 102 | assertEquals(producingItemCount.get(), consumingItemCount.get()); 103 | assertTrue(keysInMemoryQueue.isEmpty()); 104 | 105 | System.out.println("Consuming test result:"); 106 | System.out.printf("Total test time = %d ns.\n", duration); 107 | System.out.printf("Total item count = %d\n", ITEM_COUNT); 108 | System.out.printf("Consumer thread number = %d\n", CONSUMER_COUNT); 109 | System.out.printf("Item message length = %d bytes\n", STRING_LEN); 110 | System.out.printf("Total consuming time = %d ns.\n", totalConsumingTime); 111 | System.out.printf("Average consuming time = %d ns.\n", totalConsumingTime / CONSUMER_COUNT); 112 | System.out.println("-----------------------------------------------"); 113 | } 114 | 115 | private void doRunProduceMixedConsume() throws InterruptedException { 116 | CountDownLatch allLatch = new CountDownLatch(PRODUCER_COUNT + CONSUMER_COUNT); 117 | BlockingQueue producerResults = new LinkedBlockingQueue(); 118 | BlockingQueue consumerResults = new LinkedBlockingQueue(); 119 | 120 | long totalProducingTime = 0; 121 | long totalConsumingTime = 0; 122 | 123 | long start = System.nanoTime(); 124 | //run testing 125 | for (int i = 0; i < PRODUCER_COUNT; i++) { 126 | Producer p = new Producer(allLatch, producerResults); 127 | p.start(); 128 | } 129 | 130 | for (int i = 0; i < CONSUMER_COUNT; i++) { 131 | Consumer c = new Consumer(allLatch, consumerResults); 132 | c.start(); 133 | } 134 | 135 | //verify and report 136 | for (int i = 0; i < PRODUCER_COUNT; i++) { 137 | Result result = producerResults.take(); 138 | assertEquals(result.status, Status.SUCCESS); 139 | totalProducingTime += result.duration; 140 | } 141 | 142 | for (int i = 0; i < CONSUMER_COUNT; i++) { 143 | Result result = consumerResults.take(); 144 | assertEquals(result.status, Status.SUCCESS); 145 | totalConsumingTime += result.duration; 146 | } 147 | 148 | long duration = System.nanoTime() - start; 149 | 150 | assertEquals(producingItemCount.get(), consumingItemCount.get()); 151 | 152 | System.out.println("-----------------------------------------------"); 153 | System.out.printf("Total test time = %d ns.\n", duration); 154 | System.out.printf("Total item count = %d\n", ITEM_COUNT); 155 | System.out.printf("Producer thread number = %d\n", PRODUCER_COUNT); 156 | System.out.printf("Consumer thread number = %d\n", CONSUMER_COUNT); 157 | System.out.printf("Item message length = %d bytes\n", STRING_LEN); 158 | System.out.printf("Total consuming time = %d ns.\n", totalConsumingTime); 159 | System.out.printf("Average consuming time = %d ns.\n", totalConsumingTime / CONSUMER_COUNT); 160 | System.out.printf("Total producing time = %d ns.\n", totalProducingTime); 161 | System.out.printf("Average producing time = %d ns.\n", totalProducingTime / PRODUCER_COUNT); 162 | System.out.println("-----------------------------------------------"); 163 | } 164 | 165 | private class Producer extends Thread { 166 | private final CountDownLatch latch; 167 | private final Queue resultQueue; 168 | 169 | public Producer(CountDownLatch latch, Queue resultQueue) { 170 | this.latch = latch; 171 | this.resultQueue = resultQueue; 172 | } 173 | 174 | public void run() { 175 | Result result = new Result(); 176 | try { 177 | latch.countDown(); 178 | latch.await(); 179 | 180 | long start = System.nanoTime(); 181 | while (true) { 182 | int count = producingItemCount.incrementAndGet(); 183 | if (count > ITEM_COUNT) 184 | break; 185 | 186 | String string = TestUtil.randomString(STRING_LEN); 187 | 188 | keysInMemoryQueue.offer(string); 189 | cache.put(string, string); 190 | } 191 | result.status = Status.SUCCESS; 192 | result.duration = System.nanoTime() - start; 193 | } catch (Exception e) { 194 | e.printStackTrace(); 195 | result.status = Status.ERROR; 196 | } 197 | resultQueue.offer(result); 198 | } 199 | } 200 | 201 | private class Consumer extends Thread { 202 | private final CountDownLatch latch; 203 | private final Queue resultQueue; 204 | 205 | public Consumer(CountDownLatch latch, Queue resultQueue) { 206 | this.latch = latch; 207 | this.resultQueue = resultQueue; 208 | } 209 | 210 | public void run() { 211 | Result result = new Result(); 212 | result.status = Status.SUCCESS; 213 | try { 214 | latch.countDown(); 215 | latch.await(); 216 | 217 | long start = System.nanoTime(); 218 | while (true) { 219 | int count = consumingItemCount.getAndIncrement(); 220 | if (count >= ITEM_COUNT) 221 | break; 222 | 223 | String key = keysInMemoryQueue.take(); 224 | if (key != null && !key.isEmpty()) { 225 | String value = cache.get(key); 226 | // wait a moment for k/v to be put in the Cache 227 | // may cause dead lock 228 | while (value == null) { 229 | value = cache.get(key); 230 | } 231 | if (!key.equals(value)) { 232 | result.status = Status.ERROR; 233 | } 234 | } 235 | } 236 | result.duration = System.nanoTime() - start; 237 | } catch (Exception e) { 238 | e.printStackTrace(); 239 | result.status = Status.ERROR; 240 | } 241 | resultQueue.offer(result); 242 | } 243 | } 244 | 245 | private static enum Status { 246 | ERROR, 247 | SUCCESS 248 | } 249 | 250 | private static class Result { 251 | Status status; 252 | long duration; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/lrucache/v2/LruCacheV2Test.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.lrucache.v2; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | */ 9 | public class LruCacheV2Test { 10 | @Test 11 | public void test1() { 12 | LruCacheV2 cache = new LruCacheV2<>(5); 13 | cache.put(1, 1); 14 | cache.put(2, 2); 15 | cache.put(3, 3); 16 | cache.put(4, 4); 17 | cache.put(5, 5); 18 | 19 | Assert.assertTrue(cache.get(1) == 1 && cache.get(2) == 2 && cache.get(4) == 4); 20 | } 21 | 22 | @Test 23 | public void test2() { 24 | LruCacheV2 cache = new LruCacheV2<>(5); 25 | cache.put(1, 1); 26 | cache.put(2, 2); 27 | cache.put(3, 3); 28 | cache.put(4, 4); 29 | cache.put(5, 5); 30 | 31 | cache.put(6, 6); 32 | 33 | Assert.assertEquals(cache.size(), 5); 34 | } 35 | 36 | @Test 37 | public void test3() { 38 | LruCacheV2 cache = new LruCacheV2<>(5); 39 | cache.put(1, 1); 40 | cache.put(2, 2); 41 | cache.put(3, 3); 42 | cache.put(4, 4); 43 | cache.put(5, 5); 44 | 45 | cache.put(6, 6); 46 | 47 | cache.put(7, 7); 48 | 49 | Assert.assertTrue(cache.get(4) == 4 && cache.get(6) == 6 && cache.get(7) == 7); 50 | } 51 | 52 | @Test 53 | public void test4() { 54 | LruCacheV2 cache = new LruCacheV2<>(5); 55 | cache.put(1, 1); 56 | cache.put(2, 2); 57 | cache.put(3, 3); 58 | cache.put(4, 4); 59 | cache.put(5, 5); 60 | 61 | cache.put(6, 6); 62 | cache.put(7, 7); 63 | 64 | Assert.assertTrue(cache.get(1) == null); 65 | } 66 | 67 | @Test 68 | public void test5() { 69 | LruCacheV2 cache = new LruCacheV2<>(3); 70 | cache.put(1, 1); 71 | cache.put(2, 2); 72 | cache.put(1, 1); 73 | cache.put(3, 3); 74 | cache.put(4, 4); 75 | 76 | Assert.assertTrue(cache.get(1) == 1); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/lrucache/v3/LruCacheV3Test.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.lrucache.v3; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | /** 7 | * Created on Jul, 2020 by @author bobo 8 | */ 9 | public class LruCacheV3Test { 10 | @Test 11 | public void test1() { 12 | LruCacheV3 cache = new LruCacheV3<>(1,5); 13 | cache.put(1, 1); 14 | cache.put(2, 2); 15 | cache.put(3, 3); 16 | cache.put(4, 4); 17 | cache.put(5, 5); 18 | 19 | Assert.assertTrue(cache.get(1) == 1 && cache.get(2) == 2 && cache.get(4) == 4); 20 | } 21 | 22 | @Test 23 | public void test2() { 24 | LruCacheV3 cache = new LruCacheV3<>(1,5); 25 | cache.put(1, 1); 26 | cache.put(2, 2); 27 | cache.put(3, 3); 28 | cache.put(4, 4); 29 | cache.put(5, 5); 30 | 31 | cache.put(6, 6); 32 | 33 | Assert.assertEquals(cache.size(), 5); 34 | } 35 | 36 | @Test 37 | public void test3() { 38 | LruCacheV3 cache = new LruCacheV3<>(1,5); 39 | cache.put(1, 1); 40 | cache.put(2, 2); 41 | cache.put(3, 3); 42 | cache.put(4, 4); 43 | cache.put(5, 5); 44 | 45 | cache.put(6, 6); 46 | 47 | cache.put(7, 7); 48 | 49 | Assert.assertTrue(cache.get(4) == 4 && cache.get(6) == 6 && cache.get(7) == 7); 50 | } 51 | 52 | @Test 53 | public void test4() { 54 | LruCacheV3 cache = new LruCacheV3<>(1, 5); 55 | cache.put(1, 1); 56 | cache.put(2, 2); 57 | cache.put(3, 3); 58 | cache.put(4, 4); 59 | cache.put(5, 5); 60 | 61 | cache.put(6, 6); 62 | cache.put(7, 7); 63 | 64 | Assert.assertTrue(cache.get(1) == null); 65 | } 66 | 67 | @Test 68 | public void test5() { 69 | LruCacheV3 cache = new LruCacheV3<>(1,3); 70 | cache.put(1, 1); 71 | cache.put(2, 2); 72 | cache.put(1, 1); 73 | cache.put(3, 3); 74 | cache.put(4, 4); 75 | 76 | Assert.assertTrue(cache.get(1) == 1); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/okcache/CacheImplTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * Created on Jul, 2020 by @author bobo 12 | */ 13 | public class CacheImplTest { 14 | public Cache cache5() { 15 | Cache cache = CacheBuilder.newBuilder(Integer.class, String.class) 16 | .expireAfterAccess(1000, TimeUnit.MILLISECONDS) 17 | .expireAfterWrite(1500, TimeUnit.MILLISECONDS) 18 | .build(); 19 | cache.put(1, "A"); 20 | cache.put(2, "B"); 21 | cache.put(3, "C"); 22 | cache.put(4, "D"); 23 | cache.put(5, "E"); 24 | return cache; 25 | } 26 | 27 | public Cache cache5b() { 28 | Cache cache = CacheBuilder.newBuilder(Integer.class, String.class) 29 | .expireAfterWrite(1500, TimeUnit.MILLISECONDS) 30 | .build(); 31 | cache.put(1, "A"); 32 | cache.put(2, "B"); 33 | cache.put(3, "C"); 34 | cache.put(4, "D"); 35 | cache.put(5, "E"); 36 | return cache; 37 | } 38 | 39 | @Test 40 | public void testGet() throws InterruptedException { 41 | Cache cache = cache5(); 42 | Assert.assertEquals(cache.get(1), "A"); 43 | Assert.assertEquals(cache.get(2), "B"); 44 | Assert.assertEquals(cache.get(3), "C"); 45 | Assert.assertEquals(cache.get(4), "D"); 46 | Assert.assertEquals(cache.get(5), "E"); 47 | Thread.sleep(2000); 48 | Assert.assertEquals(cache.get(1), null); 49 | Assert.assertEquals(cache.get(2), null); 50 | Assert.assertEquals(cache.get(3), null); 51 | Assert.assertEquals(cache.get(4), null); 52 | Assert.assertEquals(cache.get(5), null); 53 | } 54 | 55 | @Test 56 | public void testGet2() { 57 | Cache cache = cache5(); 58 | Assert.assertEquals(cache.get(1), "A"); 59 | cache.put(1, "W"); 60 | Assert.assertEquals(cache.get(1), "W"); 61 | } 62 | 63 | @Test 64 | public void testGet3() throws InterruptedException { 65 | Cache cache = cache5(); 66 | Assert.assertEquals(cache.get(1), "A"); 67 | Assert.assertEquals(cache.get(2), "B"); 68 | Thread.sleep(800); 69 | Assert.assertEquals(cache.get(1), "A"); 70 | Assert.assertEquals(cache.get(2, Cache.UpdateTimestamp.Write), "B"); 71 | Thread.sleep(800); 72 | Assert.assertEquals(cache.get(1), null); 73 | Assert.assertEquals(cache.get(2), "B"); 74 | } 75 | 76 | @Test 77 | public void testGet4() throws InterruptedException { 78 | Cache cache = cache5b(); 79 | Assert.assertEquals(cache.get(1), "A"); 80 | Assert.assertEquals(cache.get(2), "B"); 81 | Thread.sleep(1000); 82 | Assert.assertEquals(cache.get(1), "A"); 83 | Assert.assertEquals(cache.get(2, Cache.UpdateTimestamp.Write), "B"); 84 | Thread.sleep(1000); 85 | Assert.assertEquals(cache.get(1), null); 86 | Assert.assertEquals(cache.get(2), "B"); 87 | Thread.sleep(1000); 88 | Assert.assertEquals(cache.get(1), null); 89 | Assert.assertEquals(cache.get(2), null); 90 | } 91 | 92 | @Test 93 | public void testPut() { 94 | Cache cache = cache5(); 95 | cache.put(6, "F"); 96 | Assert.assertEquals(cache.get(6), "F"); 97 | } 98 | 99 | @Test 100 | public void testPut2() { 101 | Cache cache = cache5(); 102 | String old = cache.put(1, "W"); 103 | Assert.assertEquals(old, "A"); 104 | Assert.assertEquals(cache.get(1), "W"); 105 | } 106 | 107 | @Test 108 | public void testPut3() throws InterruptedException { 109 | Cache cache = cache5(); 110 | cache.put(7, "7A", ExpirationPolicy.afterWrite(3000)); 111 | cache.put(8,"8A",ExpirationPolicy.afterCreate(5000)); 112 | cache.put(9,"9A",ExpirationPolicy.never()); 113 | 114 | Assert.assertEquals(cache.get(1), "A"); 115 | Assert.assertEquals(cache.get(7), "7A"); 116 | Assert.assertEquals(cache.get(8), "8A"); 117 | Assert.assertEquals(cache.get(9), "9A"); 118 | 119 | Thread.sleep(1001); 120 | 121 | Assert.assertEquals(cache.get(1), null); 122 | Assert.assertEquals(cache.get(7), "7A"); 123 | Assert.assertEquals(cache.get(8), "8A"); 124 | Assert.assertEquals(cache.get(9), "9A"); 125 | 126 | Thread.sleep(2000); 127 | 128 | Assert.assertEquals(cache.get(1), null); 129 | Assert.assertEquals(cache.get(7), null); 130 | Assert.assertEquals(cache.get(8), "8A"); 131 | Assert.assertEquals(cache.get(9), "9A"); 132 | 133 | Thread.sleep(2000); 134 | 135 | Assert.assertEquals(cache.get(1), null); 136 | Assert.assertEquals(cache.get(7), null); 137 | Assert.assertEquals(cache.get(8), null); 138 | Assert.assertEquals(cache.get(9), "9A"); 139 | } 140 | 141 | @Test 142 | public void testPutIfAbsent() throws InterruptedException { 143 | Cache cache = cache5(); 144 | String current = cache.putIfAbsent(1, "W"); 145 | Assert.assertEquals(current, "A"); 146 | Assert.assertEquals(cache.get(1), "A"); 147 | 148 | current = cache.putIfAbsent(6, "F"); 149 | Assert.assertEquals(current, null); 150 | Assert.assertEquals(cache.get(6), "F"); 151 | 152 | Thread.sleep(2000); 153 | current = cache.putIfAbsent(1, "W"); 154 | Assert.assertEquals(current, null); 155 | Assert.assertEquals(cache.get(1), "W"); 156 | 157 | } 158 | 159 | @Test 160 | public void testPutAll() { 161 | Map map = new HashMap(); 162 | map.put(11, "AA"); 163 | map.put(12, "AB"); 164 | map.put(13, "AC"); 165 | map.put(14, "AD"); 166 | map.put(15, "AE"); 167 | 168 | Cache cache = cache5(); 169 | cache.putAll(map); 170 | 171 | Assert.assertEquals(cache.get(11), "AA"); 172 | Assert.assertEquals(cache.get(12), "AB"); 173 | Assert.assertEquals(cache.get(13), "AC"); 174 | Assert.assertEquals(cache.get(14), "AD"); 175 | Assert.assertEquals(cache.get(15), "AE"); 176 | } 177 | 178 | @Test 179 | public void testReplace() throws InterruptedException { 180 | Cache cache = cache5(); 181 | String old = cache.replace(6, "F"); 182 | Assert.assertEquals(old, null); 183 | Assert.assertEquals(cache.get(6), null); 184 | 185 | old = cache.replace(1, "W"); 186 | Assert.assertEquals(old, "A"); 187 | Assert.assertEquals(cache.get(1), "W"); 188 | 189 | Thread.sleep(2000); 190 | 191 | old = cache.replace(1, "A"); 192 | Assert.assertEquals(old, null); 193 | Assert.assertEquals(cache.get(1), null); 194 | } 195 | 196 | @Test 197 | public void testReplace2() throws InterruptedException { 198 | Cache cache = cache5(); 199 | boolean success = cache.replace(6, "E", "F"); 200 | Assert.assertEquals(success, false); 201 | Assert.assertEquals(cache.get(6), null); 202 | 203 | success = cache.replace(1, "B", "W"); 204 | Assert.assertEquals(success, false); 205 | Assert.assertEquals(cache.get(1), "A"); 206 | 207 | success = cache.replace(1, "A", "W"); 208 | Assert.assertEquals(success, true); 209 | Assert.assertEquals(cache.get(1), "W"); 210 | 211 | Thread.sleep(2000); 212 | 213 | success = cache.replace(1, "W", "A"); 214 | Assert.assertEquals(success, false); 215 | Assert.assertEquals(cache.get(1), null); 216 | } 217 | 218 | @Test 219 | public void testRemove() throws InterruptedException { 220 | Cache cache = cache5(); 221 | String old = cache.remove(1); 222 | Assert.assertEquals(old, "A"); 223 | Assert.assertEquals(cache.get(1), null); 224 | 225 | old = cache.remove(6); 226 | Assert.assertEquals(old, null); 227 | Assert.assertEquals(cache.get(6), null); 228 | 229 | Thread.sleep(2000); 230 | 231 | old = cache.remove(2); 232 | Assert.assertEquals(old, null); 233 | Assert.assertEquals(cache.get(2), null); 234 | } 235 | 236 | @Test 237 | public void testRemove2() throws InterruptedException { 238 | Cache cache = cache5(); 239 | boolean success = cache.remove(6, "E"); 240 | Assert.assertEquals(success, false); 241 | Assert.assertEquals(cache.get(6), null); 242 | 243 | success = cache.remove(1, "B"); 244 | Assert.assertEquals(success, false); 245 | Assert.assertEquals(cache.get(1), "A"); 246 | 247 | success = cache.remove(1, "A"); 248 | Assert.assertEquals(success, true); 249 | Assert.assertEquals(cache.get(1), null); 250 | 251 | Thread.sleep(2000); 252 | 253 | success = cache.remove(2, "B"); 254 | Assert.assertEquals(success, false); 255 | Assert.assertEquals(cache.get(2), null); 256 | } 257 | 258 | @Test 259 | public void testContains() throws InterruptedException { 260 | Cache cache = cache5(); 261 | Assert.assertEquals(cache.contains(1), true); 262 | Assert.assertEquals(cache.contains(6), false); 263 | 264 | Thread.sleep(2000); 265 | 266 | Assert.assertEquals(cache.contains(1), false); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/okcache/CachePerfGetsTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.Serializable; 6 | import java.util.Map; 7 | import java.util.Random; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | /** 13 | * Created on Jul, 2020 by @author bobo 14 | */ 15 | public class CachePerfGetsTest { 16 | @Test 17 | public void stressGet(){ 18 | final Cache cache = CacheBuilder.newBuilder(Serializable.class,Serializable.class) 19 | .expireAfterWrite(1000, TimeUnit.MINUTES) 20 | .maximumSize(200000) 21 | .concurrencyLevel(25) 22 | .build(); 23 | 24 | final int eCount = 20000; 25 | final Serializable[] ks = new Serializable[eCount]; 26 | for (int i = 0; i < eCount; i++) { 27 | cache.put("k" + i, "v" + i); 28 | ks[i] = "k" + i; 29 | } 30 | 31 | 32 | final AtomicInteger totalCount = new AtomicInteger(); 33 | 34 | int tCount = 25; 35 | final int time = 1000*60; 36 | Thread[] ts = new Thread[tCount]; 37 | for (int i = 0; i < tCount; i++) { 38 | ts[i] = new Thread(){ 39 | int count = 0; 40 | @Override 41 | public void run() { 42 | Random random = new Random(); 43 | long start = System.currentTimeMillis(); 44 | while(true){ 45 | int i = random.nextInt(eCount); 46 | cache.get(ks[i]); 47 | count++; 48 | if(System.currentTimeMillis() - start >= time) break; 49 | } 50 | totalCount.addAndGet(count); 51 | } 52 | }; 53 | } 54 | 55 | for (Thread t : ts) { 56 | t.start(); 57 | } 58 | 59 | for (Thread t : ts) { 60 | try { 61 | t.join(); 62 | } catch (InterruptedException e) { 63 | e.printStackTrace(); 64 | } 65 | } 66 | 67 | System.out.println("Total Count:" + totalCount.get()); 68 | System.out.println("Ops/Sec:" + (totalCount.get()/(time/1000))); 69 | 70 | } 71 | 72 | @Test 73 | public void stressConcurrentHashMap(){ 74 | final Map cache = new ConcurrentHashMap(1000,0.7f, 4); 75 | 76 | final int eCount = 20000; 77 | final Serializable[] ks = new Serializable[eCount]; 78 | for (int i = 0; i < eCount; i++) { 79 | cache.put("k" + i, "v" + i); 80 | ks[i] = "k" + i; 81 | } 82 | 83 | 84 | final AtomicInteger totalCount = new AtomicInteger(); 85 | 86 | int tCount = 25; 87 | final int time = 1000*60; 88 | Thread[] ts = new Thread[tCount]; 89 | for (int i = 0; i < tCount; i++) { 90 | ts[i] = new Thread(){ 91 | int count = 0; 92 | @Override 93 | public void run() { 94 | Random random = new Random(); 95 | long start = System.currentTimeMillis(); 96 | while(true){ 97 | int i = random.nextInt(eCount); 98 | cache.put(ks[i], "v" + i); 99 | //cache.get(ks[i]); 100 | count++; 101 | if(System.currentTimeMillis() - start >= time) break; 102 | } 103 | totalCount.addAndGet(count); 104 | } 105 | }; 106 | } 107 | 108 | for (Thread t : ts) { 109 | t.start(); 110 | } 111 | 112 | for (Thread t : ts) { 113 | try { 114 | t.join(); 115 | } catch (InterruptedException e) { 116 | e.printStackTrace(); 117 | } 118 | } 119 | 120 | System.out.println("Total Count:" + totalCount.get()); 121 | System.out.println("Ops/Sec:" + (totalCount.get()/(time/1000))); 122 | 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/okcache/CachePerfTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import com.spring2go.bigcache.BigCache; 4 | import com.spring2go.bigcache.CacheConfig; 5 | import com.spring2go.bigcache.utils.TestUtil; 6 | import com.spring2go.okcache.store.BigCacheStore; 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | 10 | import java.io.IOException; 11 | import java.util.concurrent.*; 12 | 13 | /** 14 | * Created on Jul, 2020 by @author bobo 15 | */ 16 | public class CachePerfTest { 17 | public Cache cache2000() throws IOException { 18 | CacheConfig config = new CacheConfig(); 19 | config.setStorageMode(CacheConfig.StorageMode.OffHeapPlusFile) 20 | .setCapacityPerBlock(16 * 1024 * 1024) 21 | .setMaxOffHeapMemorySize(16 * 1024 * 1024) 22 | .setMergeInterval(2 * 1000) 23 | .setPurgeInterval(2 * 1000); 24 | BigCache bigcache = new BigCache(TestUtil.TEST_BASE_DIR, config); 25 | BigCacheStore evictStore = new BigCacheStore(bigcache); 26 | Cache cache = CacheBuilder.newBuilder(Integer.class, String.class) 27 | .expireAfterAccess(1000*30, TimeUnit.MILLISECONDS) 28 | .maximumSize(2000) 29 | .addEvictStore(evictStore) 30 | .build(); 31 | 32 | for (int i = 1; i <= 200; i++) { 33 | cache.put(i, "V" + i); 34 | } 35 | return cache; 36 | } 37 | 38 | @Test 39 | public void testPutAndGet() throws IOException, ExecutionException, InterruptedException { 40 | int count = 300000; 41 | int threadCount = 8; 42 | 43 | final Cache cache = cache2000(); 44 | ExecutorService executor = Executors.newFixedThreadPool(threadCount); 45 | Future[] futures = new Future[count]; 46 | 47 | long start = System.nanoTime(); 48 | for (int i = 0; i < count; i++) { 49 | final int finalI = i; 50 | futures[i] = executor.submit(new Callable() { 51 | @Override 52 | public Object call() throws Exception { 53 | cache.put(finalI,"V" + finalI); 54 | return finalI; 55 | } 56 | }); 57 | } 58 | 59 | for (Future future : futures) { 60 | future.get(); 61 | } 62 | 63 | long end = System.nanoTime(); 64 | 65 | System.out.println("Puts Cost:" + (end - start) + " ns, average:" + ((end - start)/count) + " ns"); 66 | System.out.println(cache.stats()); 67 | 68 | Thread.sleep(5000); 69 | System.out.println(cache.stats()); 70 | 71 | start = System.nanoTime(); 72 | for (int i = 0; i < count; i++) { 73 | final int finalI = i; 74 | futures[i] = executor.submit(new Callable() { 75 | @Override 76 | public Object call() throws Exception { 77 | return cache.get(finalI); 78 | } 79 | }); 80 | } 81 | 82 | String[] res = new String[count]; 83 | for (int i = 0; i < count; i++) { 84 | res[i] = (String) futures[i].get(); 85 | } 86 | 87 | end = System.nanoTime(); 88 | 89 | System.out.println("Gets Cost:" + (end - start) + " ns, average:" + ((end - start)/count) + " ns"); 90 | System.out.println(cache.stats()); 91 | 92 | Thread.sleep(5000); 93 | System.out.println(cache.stats()); 94 | 95 | for (int i = 0; i < count; i++) { 96 | Assert.assertEquals(res[i],"V" + i); 97 | } 98 | 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/okcache/CacheWithEvictStoreTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import com.spring2go.bigcache.BigCache; 4 | import com.spring2go.bigcache.CacheConfig; 5 | import com.spring2go.bigcache.utils.TestUtil; 6 | import com.spring2go.okcache.store.BigCacheStore; 7 | import org.junit.After; 8 | import org.junit.Assert; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | 12 | import java.io.IOException; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * Created on Jul, 2020 by @author bobo 17 | */ 18 | public class CacheWithEvictStoreTest { 19 | private Cache cache; 20 | private BigCache bigcache; 21 | 22 | @Before 23 | public void setUp() { 24 | try { 25 | CacheConfig config = new CacheConfig(); 26 | config.setStorageMode(CacheConfig.StorageMode.OffHeapPlusFile) 27 | .setCapacityPerBlock(16 * 1024 * 1024) 28 | .setMaxOffHeapMemorySize(16 * 1024 * 1024) 29 | .setMergeInterval(2 * 1000) 30 | .setPurgeInterval(2 * 1000); 31 | bigcache = new BigCache(TestUtil.TEST_BASE_DIR, config); 32 | 33 | BigCacheStore evictStore = new BigCacheStore(bigcache); 34 | cache = CacheBuilder.newBuilder(Integer.class, String.class) 35 | .expireAfterAccess(1000*30, TimeUnit.MILLISECONDS) 36 | .maximumSize(100) 37 | .addEvictStore(evictStore) 38 | .build(); 39 | 40 | for (int i = 1; i <= 2000000; i++) { 41 | cache.put(i, "V" + i); 42 | } 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | } 46 | } 47 | 48 | @After 49 | public void tearDown() throws IOException { 50 | if (bigcache != null) { 51 | bigcache.close(); 52 | } 53 | } 54 | 55 | @Test 56 | public void testGet() throws IOException, InterruptedException { 57 | Thread.sleep(5000); 58 | Assert.assertEquals(cache.get(1), "V1"); 59 | Assert.assertEquals(cache.get(200), "V200"); 60 | System.out.println(cache.stats()); 61 | } 62 | @Test 63 | public void testGet2() throws IOException, InterruptedException { 64 | Thread.sleep(5000); 65 | long start = System.nanoTime(); 66 | for (int i = 1; i <= 130; i++) { 67 | Assert.assertEquals(cache.get(i), "V"+i); 68 | } 69 | System.out.println("AverageCost:" + (System.nanoTime() -start)/130); 70 | System.out.println(cache.stats()); 71 | Thread.sleep(5000); 72 | System.out.println(cache.stats()); 73 | } 74 | 75 | @Test 76 | public void testPut() throws IOException, InterruptedException { 77 | Thread.sleep(5000); 78 | cache.put(1,"V1M"); 79 | Assert.assertEquals(cache.get(1), "V1M"); 80 | Assert.assertEquals(cache.get(200), "V200"); 81 | System.out.println(cache.stats()); 82 | } 83 | 84 | @Test 85 | public void testPutIfAbsent() throws IOException, InterruptedException { 86 | Thread.sleep(5000); 87 | String old = cache.putIfAbsent(1, "V1M"); 88 | Assert.assertEquals(old, "V1"); 89 | Assert.assertEquals(cache.get(1), "V1"); 90 | Assert.assertEquals(cache.get(200), "V200"); 91 | System.out.println(cache.stats()); 92 | } 93 | 94 | @Test 95 | public void testReplace() throws IOException, InterruptedException { 96 | Thread.sleep(5000); 97 | String old = cache.replace(100000000,"V1M"); 98 | Assert.assertEquals(old, null); 99 | Assert.assertEquals(cache.get(100000000), null); 100 | 101 | old = cache.replace(1,"V1M"); 102 | Assert.assertEquals(old, "V1"); 103 | Assert.assertEquals(cache.get(1), "V1M"); 104 | 105 | old = cache.replace(200,"V200M"); 106 | Assert.assertEquals(old, "V200"); 107 | Assert.assertEquals(cache.get(200), "V200M"); 108 | 109 | System.out.println(cache.stats()); 110 | } 111 | 112 | @Test 113 | public void testReplace2() throws IOException, InterruptedException { 114 | Thread.sleep(5000); 115 | boolean res = cache.replace(100000000,"V1000", "V1000M"); 116 | Assert.assertFalse(res); 117 | Assert.assertEquals(cache.get(100000000), null); 118 | 119 | res = cache.replace(1,"V1W", "V1M"); 120 | Assert.assertFalse(res); 121 | Assert.assertEquals(cache.get(1), "V1"); 122 | 123 | res = cache.replace(2,"V2", "V2M"); 124 | Assert.assertTrue(res); 125 | Assert.assertEquals(cache.get(2), "V2M"); 126 | 127 | System.out.println(cache.stats()); 128 | } 129 | 130 | @Test 131 | public void testRemove() throws IOException, InterruptedException { 132 | Thread.sleep(5000); 133 | String old = cache.remove(100000000); 134 | Assert.assertEquals(old, null); 135 | Assert.assertEquals(cache.get(100000000), null); 136 | 137 | old = cache.remove(1); 138 | Assert.assertEquals(old, "V1"); 139 | Assert.assertEquals(cache.get(1), null); 140 | 141 | old = cache.remove(200); 142 | Assert.assertEquals(old, "V200"); 143 | Assert.assertEquals(cache.get(200), null); 144 | 145 | System.out.println(cache.stats()); 146 | } 147 | 148 | @Test 149 | public void testRemove2() throws IOException, InterruptedException { 150 | Thread.sleep(5000); 151 | boolean res = cache.remove(100000000,"V1000"); 152 | Assert.assertFalse(res); 153 | Assert.assertEquals(cache.get(100000000), null); 154 | 155 | res = cache.remove(1,"V10"); 156 | Assert.assertFalse(res); 157 | Assert.assertEquals(cache.get(1), "V1"); 158 | 159 | res = cache.remove(2,"V2"); 160 | Assert.assertTrue(res); 161 | Assert.assertEquals(cache.get(2), null); 162 | 163 | System.out.println(cache.stats()); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/test/java/com/spring2go/okcache/JavaSerializationTest.java: -------------------------------------------------------------------------------- 1 | package com.spring2go.okcache; 2 | 3 | import com.spring2go.okcache.impl.HashCacheEntry; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.util.Arrays; 8 | import java.util.Random; 9 | 10 | /** 11 | * Created on Jul, 2020 by @author bobo 12 | */ 13 | public class JavaSerializationTest { 14 | @Test 15 | public void perfTest() throws Exception { 16 | Serializer serializer = new JavaSerializer(); 17 | Random random = new Random(); 18 | 19 | int count = 10000; 20 | CacheEntry[] es = new CacheEntry[count]; 21 | for (int i = 0; i < count; i++) { 22 | byte[] b = new byte[random.nextInt(1024) + 1024*5]; 23 | Arrays.fill(b, (byte) random.nextInt(100)); 24 | es[i] = new HashCacheEntry(0,"key-" + i, b, ExpirationPolicy.never()); 25 | } 26 | 27 | byte[][] res = new byte[count][]; 28 | long start = System.currentTimeMillis(); 29 | for (int i = 0; i < count; i++) { 30 | res[i] = serializer.serialize(es[i]); 31 | } 32 | 33 | System.out.println("Cost(ms): " + (System.currentTimeMillis() - start)); 34 | 35 | start = System.currentTimeMillis(); 36 | for (int i = 0; i < count; i++) { 37 | serializer.deserialize(res[i]); 38 | } 39 | System.out.println("Cost(ms): " + (System.currentTimeMillis() - start)); 40 | 41 | } 42 | 43 | @Test 44 | public void test2() throws Exception { 45 | Serializer serializer = new JavaSerializer(); 46 | byte[] a1 = serializer.serialize(new Integer(1)); 47 | byte[] a2 = serializer.serialize(new Integer(1)); 48 | 49 | Assert.assertTrue(Arrays.equals(a1,a2)); 50 | 51 | } 52 | } 53 | --------------------------------------------------------------------------------