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> 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 reference;
    private final long expiryTime;

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

    public String getKey() {
        return key;
    }

    public SoftReference 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> cache = new ConcurrentHashMap<>();
    private final DelayQueue 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 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;
    }



}

 

相关测试截图:

添加:

Java使用ConcurrentHashMap实现简单的内存式缓存_第1张图片

查询

Java使用ConcurrentHashMap实现简单的内存式缓存_第2张图片

 

1分钟内查看大小

Java使用ConcurrentHashMap实现简单的内存式缓存_第3张图片

1分钟后查看大小

Java使用ConcurrentHashMap实现简单的内存式缓存_第4张图片

 

你可能感兴趣的:(Java-Core)