Java使用ConcurrentHashMap實現簡單的內存式緩存

需求說明:

實際項目中我打算把用戶和組織信息放到緩存中,基於此提出以下幾點需求:

1.數據存儲在內存中;
2.允許以鍵值對的方式存儲對象類數據並帶有過期策略;
3.不限制內存使用,但cache也不能給我報出OutOfMemoryErrormemory異常;
4.cache要自動清理過期對象
5.線程安全


爲了滿足以上需求,本例子中主要使用了以下技術:
1.ConcurrentHashMap
2.DelayQueue
3.SoftReference
4.Thread

5.java8的Optional及函數式編程

這裏提供2中編碼實現,先定義一個接口,用於規範和統一方法:

 

package com.dylan.springboot.helloweb.cache;

/**
 * @Description: 自定義緩存接口
 * @Author laoxu
 * @Date 2019/7/27 13:45
 **/
public interface ICache {
    void add(String key, Object value, long periodInMillis);

    void remove(String key);

    Object get(String key);

    void clear();

    long size();

}


方案1:

package com.dylan.springboot.helloweb.cache;

/**
 * @Description: 帶過期時間的緩存對象
 * @Author laoxu
 * @Date 2019/7/27 14:27
 **/
public class CacheObject {
    private Object value;
    private long expiryTime;

    public CacheObject(Object value, long expiryTime) {
        this.value = value;
        this.expiryTime = expiryTime;
    }

    public Object getValue() {
        return value;
    }

    boolean isExpired() {
        return System.currentTimeMillis() > expiryTime;
    }


}
package com.dylan.springboot.helloweb.cache;


import java.lang.ref.SoftReference;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Description: 方案1,緩存實現
 * @Author laoxu
 * @Date 2019/7/27 13:47
 **/
// @Component
public class CacheImpl implements ICache {
    private static final int CLEAN_UP_PERIOD_IN_SEC = 5;

    private final ConcurrentHashMap<String, SoftReference<CacheObject>> cache = new ConcurrentHashMap<>();

    public CacheImpl() {
        Thread cleanerThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(CLEAN_UP_PERIOD_IN_SEC * 1000);
                    cache.entrySet().removeIf(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(CacheObject::isExpired).orElse(false));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        cleanerThread.setDaemon(true);
        cleanerThread.start();
    }

    @Override
    public void add(String key, Object value, long periodInMillis) {
        if (key == null) {
            return;
        }
        if (value == null) {
            cache.remove(key);
        } else {
            long expiryTime = System.currentTimeMillis() + periodInMillis;
            cache.put(key, new SoftReference<>(new CacheObject(value, expiryTime)));
        }
    }

    @Override
    public void remove(String key) {
        cache.remove(key);
    }

    @Override
    public Object get(String key) {
        return Optional.ofNullable(cache.get(key)).map(SoftReference::get).filter(cacheObject -> !cacheObject.isExpired()).map(CacheObject::getValue).orElse(null);
    }

    @Override
    public void clear() {
        cache.clear();
    }

    @Override
    public long size() {
        return cache.entrySet().stream().filter(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(cacheObject -> !cacheObject.isExpired()).orElse(false)).count();
    }



}

 

此方案有2大缺陷:

1.如果map中存儲了大量的對象那麼掃描這些對象並清理需要花不少時間;
2.此處的size()方法將花費O(n)時間因爲它必須過濾掉過期對象。


方案2(推薦):

 

package com.dylan.springboot.helloweb.cache;

import java.lang.ref.SoftReference;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @Description: 帶key和過期時間的緩存對象
 * @Author laoxu
 * @Date 2019/7/27 15:28
 **/
public class DelayedCacheObject implements Delayed {
    private final String key;
    private final SoftReference<Object> reference;
    private final long expiryTime;

    public DelayedCacheObject(String key, SoftReference<Object> reference, long expiryTime) {
        this.key = key;
        this.reference = reference;
        this.expiryTime = expiryTime;
    }

    public String getKey() {
        return key;
    }

    public SoftReference<Object> getReference() {
        return reference;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(expiryTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        return Long.compare(expiryTime, ((DelayedCacheObject) o).expiryTime);
    }
}

 

package com.dylan.springboot.helloweb.cache;

import org.springframework.stereotype.Component;

import java.lang.ref.SoftReference;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;

/**
 * @Description: 方案2,緩存實現
 * @Author laoxu
 * @Date 2019/7/27 15:24
 **/
@Component
public class InMemoryCacheWithDelayQueue implements ICache {
    private final ConcurrentHashMap<String, SoftReference<Object>> cache = new ConcurrentHashMap<>();
    private final DelayQueue<DelayedCacheObject> cleaningUpQueue = new DelayQueue<>();

    public InMemoryCacheWithDelayQueue() {
        Thread cleanerThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    DelayedCacheObject delayedCacheObject = cleaningUpQueue.take();
                    cache.remove(delayedCacheObject.getKey(), delayedCacheObject.getReference());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        cleanerThread.setDaemon(true);
        cleanerThread.start();
    }

    @Override
    public void add(String key, Object value, long periodInMillis) {
        if (key == null) {
            return;
        }
        if (value == null) {
            cache.remove(key);
        } else {
            long expiryTime = System.currentTimeMillis() + periodInMillis;
            SoftReference<Object> reference = new SoftReference<>(value);
            cache.put(key, reference);
            cleaningUpQueue.put(new DelayedCacheObject(key, reference, expiryTime));
        }
    }

    @Override
    public void remove(String key) {
        cache.remove(key);
    }

    @Override
    public Object get(String key) {
        return Optional.ofNullable(cache.get(key)).map(SoftReference::get).orElse(null);
    }

    @Override
    public void clear() {
        cache.clear();
    }

    @Override
    public long size() {
        return cache.size();
    }

}

測試

 

爲了方便測試,我在spring boot中添加了controller:

package com.dylan.springboot.helloweb.controller;

import com.dylan.springboot.helloweb.cache.ICache;
import com.dylan.springboot.helloweb.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @Description: 測試緩存
 * @Author laoxu
 * @Date 2019/7/27 14:35
 **/
@RestController
@RequestMapping("/cache")
public class CacheController {

    @Autowired
    private ICache cache;

    @GetMapping("/size")
    public Long getSize(){
        return cache.size();
    }

    @PostMapping("/add")
    public String add(@RequestBody Student student){
        cache.add(student.getName(),student,60000);

        return "success";
    }

    @GetMapping("/get")
    public Student get(String name){
        Student student = (Student) cache.get(name);

        return student;
    }



}

 

相關測試截圖:

添加:

查詢

 

1分鐘內查看大小

1分鐘後查看大小

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章