线程安全的可控制最大容量且带有过期时间的本地缓存
最近在公司优化一个接口的时候打算使用一个key-value结构的本地缓存。
需要实现的功能非常简单:
1、可以控制本地缓存的最大对象数量。
2、线程安全,防止发生OOM。
3、同时支持设置单个对象的过期时间。
面对这个需求,我的选择很多,有很多框架都做的非常好,但大多数框架对我来说都太重量级了,我希望一个简单高效的实现,所以我开发了一个简单的小工具,在这里可以分享下实现思路和开发当中遇到的问题以及解决办法。
首先是key-value的结构,我底层封装了一个Map来保存数据。然后要解决线程安全问题,所以我使用了
ConcurrentHashMap这个Map的实现,关于ConcurrentHashMap这个类网上有很多介绍,我在这里就不多说了。
接下来就是需要控制最多存储的对象数量,防止本地缓存太多对象(而且对象一直都被引用,还无法被GC)造成OOM,一开始我只是简单的使用比较size和最大值来判断是否还能添加对象,但是在后来的测试发现并发量非常高的时候会多存几倍的对象,为了保证性能我还不希望加锁或使用synchronized关键字,所以我选择了AtomicInteger这个原子类巧妙的处理添加和删除方法。这个问题的解决我会在代码里详细解释。
对于过期时间实现,我参考了Redis底层对于过期部分的实现,它分为主动和被动过期,前者更节约空间后者性能更好,为此我兼容了两者的优势,采取了主动+被动的方式,在查询时判断是否过期,如果过期,清除对象同时返回null(被动)。在添加元素时判断是否还有空间,如果有正常添加,如果没有触发全量过期,之后再判断是否有空间,有就添加,没有就返回添加失败(主动)。
具体代码如下
import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; * 本地缓存 * 采用懒过期模式 在查询时才判断是否过期 * 在缓存满了的时候触发主动过期过期 * @author zhangmingxu ON 17:52 2019-05-20 public class LocalCache { private static final Logger logger = LoggerFactory.getLogger(LocalCache.class); private static final int DEFAULT_MAX_NUMBER = 100; //默认最大缓存对象数 private final Map String, Value> cache; //真正存储数据的Map,使用ConcurrentHashMap private final int maxNumber; //最大对象数 //并发控制器,很重要,防止高并发下本地缓存对象个数超过maxNumber private final AtomicInteger cur = new AtomicInteger(0); * 使用默认最大对象数100 public LocalCache() { this(DEFAULT_MAX_NUMBER); public LocalCache(int maxNumber) { this.maxNumber = maxNumber; this.cache = new ConcurrentHashMap >(maxNumber); * 添加 * 判断是否超过最大限制 如果超过触发一次全量过期 * 如果全量过期后还不够返回false * 由于1 2 不是原子的所以需要使用单独的AtomicInteger来控制 * @param key 对应的key * @param value 值 * @param expire 过期时间 单位毫秒 public boolean put(String key, Object value, long expire) { if (StringUtils.isBlank(key) || value == null || expire 0) { logger.error("本地缓存put参数异常"); return false; if (!incr()) { //如果CAS增加失败直接返回添加失败 return false; if (isOver()) { //判断是否需要过期 expireAll(); //触发一次全量过期 if (isOver()) { //二次检查 logger.error("本地缓存put时全量过期后还没有空间"); decr(); return false; putValue(key, value, expire); return true; * 获取时判断过期时间 * 在这里实现懒过期 public Object get(String key) { Value v = cache.get(key); if (v == null) { return null; if (isExpired(v)) { logger.info("本地缓存key={}已经过期", key); removeValue(key); return null; return v.value; * 判断是否过期,实现很简单 private boolean isExpired(Value v) { long current = System.currentTimeMillis(); return current - v.updateTime > v.expire; * 扫描所有的对象对需要过期的过期 private void expireAll() { logger.info("开始过期本地缓存"); for (Map.Entry String, Value> entry : cache.entrySet()) { if (isExpired(entry.getValue())) { removeValue(entry.getKey()); * 为了保证cur和Map的size时刻保持一致这里我查询了put的注释及ConcurrentHashMap底层关于put的实现。 * 发现如果put方法返回的不是null说明存在覆盖操作,如果是覆盖那么Map的size其实没有变,因为我们添加之前把cur的值增加 * 上去了所以要在这里减下来。 private void putValue(String key, Object value, long expire) { Value v = new Value(System.currentTimeMillis(), expire, value); if (cache.put(key, v) != null) {//存在覆盖 使得cur和map的size统一 decr(); * 这里也是为了保证cur和Map的size时刻保持一致只有在remove方法返回的不是null时才证明真正有对象被删除了,才需要把 * cur减下来。这里出现remove返回为null是因为可能存在并发删除,两个线程删除同一个对象只能有一个删除成功(返回不是 * null),另一个(返回null)如果也减小了cur的值,会造成cur和Map的size不一致。 private void removeValue(String key) { if (cache.remove(key) != null) { //真正删除成功了 使得cur和map的size统一 decr(); * 这里很重要,原来我使用的是cache.size() >= maxNumber; * 但是如果使用map本身的size方法会存在获取size和putValue方法不是原子的, * 可能多个线程同时都判断那时候还没执行putValue方法,线程都认为还没有满,大家都执行了putValue方法造成数据太多 private boolean isOver() { return cur.get() > maxNumber; private boolean incr() { int c = cur.get(); return cur.compareAndSet(c, ++c); * 因为CAS不一定是一定成功的 * 所以这里通过循环保证成功 private void decr() { for (; ; ) { int c = cur.get(); if (c == 0) { logger.error("LocalCache decr cur is 0"); return; if (cur.compareAndSet(c, --c)) { return; private static class Value { private long updateTime; //更新时间 private long expire; //有效期 private Object value; //真正的对象 private Value(long updateTime, long expire, Object value) { this.updateTime = updateTime; this.expire = expire; this.value = value; }
这里面最关键的就是AtomicInteger cur这个对象,它在put方法参数校验通过之后就加1(虽然当时还没有putValue),使用这个操作让其他线程在后面的isOver方法中马上感知到数量变化,不会添加过多的对象。
保证cur的值和Map的Size时刻一致也很重要,并不是只要putValue了就加一(覆盖时虽然put进去了对象但是size不变),remove了就减一(并发删除同一个对象只能有一个成功,可能多减了),平常我们在使用Map的put和remove方法时往往忽略了它们的返回值,所以我建议大家仔细阅读源代码,加深理解。
并发测试代码如下:
public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); LocalCache localCache = new LocalCache(); int n = 500; //线程数 int m = 100000; //每个线程put个数 CountDownLatch count = new CountDownLatch(n); for (int i = 0; i i++) { new Thread(() -> { for (int j = 0; j j++) { localCache.put(j + "", new Object(), 10); count.countDown(); }).start(); count.await(); System.out.println("size:" + localCache.cache.size()); System.out.println("cur:" + localCache.cur); System.out.println("耗时 " + (System.currentTimeMillis() - start));
原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/60361.html
安全相关文章
- c#子窗口与父窗口_主窗体控制子窗体的显示
- 树莓派控制摄像头_树莓派连接摄像头
- 十字路口的交通灯控制系统_十字路口红绿灯控制程序设计
- PHP会话控制简述
- Oracle控制文件查看:了解它的重要性(oracle控制文件查看)
- 控制・Oracle 并发控制:实现安全、稳定业务(oracle并发)
- 控制Linux IP登录控制:实现安全访问(linux登录ip)
- 权限Linux SVN权限控制:管理文件安全(linuxsvn文件)
- 权限MySQL IP控制:实现安全的远程访问权(mysqlip)
- Teleportal.Fish系统让用户远程控制真正访问珊瑚礁的ROV
- Linux禁用无线网卡:妥善控制网络安全(linux禁用无线网卡)
- 使用VNC远程控制Linux系统,实现便捷高效的管理(vnc控制linux)
- Linux用户权限管理指南:控制访问机制实现安全授权(linux下给用户权限)
- Oracle关闭自动扩展:了解如何控制数据库空间扩充(oracle关闭自动扩展)
- Oracle的并发控制:安全保护数据完整性(oracle的并发控制)
- MSSQL远程控制——轻松掌握安全运维(mssql远程控制)
- Linux远程控制Windows:让你的操作变得更轻松(linux远程windows)
- Oracle禁用闪回安全与控制背后的考量(oracle 关掉闪回)
- Oracle的六种约束机制有效控制数据完整性(oracle六种约束)
- 控制Oracle中表权限管理实现安全操作(oracle中表的权限)
- 华尔街英语负责人被控制:欠下12亿学费跑路、已被强制执行
- 用Python编程实现语音控制电脑