juc之ConcurrentHashMap在我工作中的实践
Map是我工作中应用比较多的数据结构之一,主要用来存储一些kv的映射信息,如果是单线程环境下我会优先使用HashMap,但是如果在多线程环境下继续使用HashMap我不确定会不会被我老大打死,为了生命安全考虑我选用了大名鼎鼎的ConcurrentHashMap。
使用背景
笔者负责过一个http推送系统,其职责是定时将生产者者插入到库中的推送任务捞出来根据推送地址进行http推送,推送时需要进行RSA签名,但是加签用的私钥是每个接收者不同的,所以意味着每次在推送时我是需要根据推送者标识(统一叫appId)去获取私钥信息的,很显然每次查库不是个好办法,这时就需要加一层缓存来解决这个问题了。
为什么使用ConcurrentHashMap
最开始我是想用Redis这种集中缓存的,操作简单,而且不用考虑多节点之间数据的一致性,后来跟负责开放平台的同学沟通了一下(接收者是ISV,公私钥信息目前是开放平台同学线下派发的),公私钥信息被改动的频率很小,线上跑了两年多目前还没有ISV提出要更换,基于这一点我决定使用本地缓存,本地缓存的优点是减少了网络IO,性能更好,缺点是多节点之间数据一致性是个问题,如果数据不一致ISV验签失败会返回错误,但推送系统有个设计是如果推送失败了会阶梯式重试,整个重试过程持续12个小时,我只要把本地缓存的过期时间调小就好,最终会拿到正确的私钥信息(后来也想过设计明确的错误码,比如ISV返回SIGN_ERROR时主动刷新本地缓存,但是这涉及到接口协议的变更,最终还是搁置了),我为什么没有选择Ehcache,Guava这种成熟的缓存框架呢?一个原因是不太熟悉,第二个原因是我这个场景比较简单,不想引入一套框架进来。
步骤分解
1.根据appId去本地缓存中查询是否存在;
2.如果存在,判断是否过期,如果不过期直接返回,否则继续执行以下逻辑;
3.删除appId对应的Value
4.查询db;
5.将查询结果放到本地缓存中;
识别问题
上面的五步是一般使用缓存的大体流程,这五步涉及到的一些细节如下:
1.过期数据如何识别
2.过期数据如何删除
3.缓存并发更新如何处理
解决问题
1.如何识别过期数据:往本地缓存塞值的时候同时将过期时间塞进去,这块是将Value做了一层包装,大体结构如下:
public class CacheFutureTask<V> extends FutureTask<V>{
private long expireTime = -1;
private long createTime = -1;
private V result;
public CacheFutureTask (Callable<V> call){
super(call);
this.createTime = System.currentTimeMillis();
this.expireTime = this.createTime+ TimeUnit.MINUTES.toMillis(30);//三十分钟过期
}
//是否过期
public boolean isExpired(){
return System.currentTimeMillis() > this.expireTime ;
}
public CacheFutureTask(Runnable runnable, V result) {
super(runnable, result);
}
@Override
public V get() throws InterruptedException, ExecutionException {
return super.get();
}
}
2.如何删除过期数据:一般的缓存框架都支持主动删除和惰性删除,主动删除主要是为了尽快释放内存,实现起来有一定复杂度,我这个场景中数据量不大,目前几十条,短期内也不会有大幅增长,所以内存并不会成为瓶颈,基于此我只是实现了惰性删除,获取之后判断下是否过期,如果过期就删除,代码如下;
CacheFutureTask<OpenIsvAppDO> cacheFutureTask = null;
cacheFutureTask = isvAppInfos.get(appId);
if(cacheFutureTask != null && cacheFutureTask.isExpired()){
logger.info("appId:{} configInfo is expired,will load from db",appId);
isvAppInfos.remove(appId);
cacheFutureTask = null;
}
3.如何避免缓存并发更新:缓存存在的意义就是减少db的访问,但是在并发环境下每个线程都有机会去更新缓存,如果不做控制在高并发环境下对db是一种摧残,所以必须要控制并发更新缓存,更新时需要加锁,更新完成释放锁,不过要注意设置合理的超时时间,否则可能会有大量线程等待,严重的时候可能会撑爆jvm内存,这块用到了ConcurrentHashMap的putIfAbsent(K key, V value) 方法来实现单线程更新缓存,使用CacheFutureTask的get(long timeout, TimeUnit unit)方法来做到超时结束等待,代码如下:
CacheFutureTask<OpenIsvAppDO> cacheFutureTask = null;
try {
//缓存惰性删除逻辑
cacheFutureTask = isvAppInfos.get(appId);
if(cacheFutureTask != null && cacheFutureTask.isExpired()){
logger.info("appId:{} configInfo is expire,will load from db",appId);
isvAppInfos.remove(appId);
cacheFutureTask = null;
}
//如果本地缓存中不存在appId对应条目,就构造一个CacheFutureTask尝试插入
if(cacheFutureTask == null) {
Callable<OpenIsvAppDO> call = new Callable<OpenIsvAppDO>() {
@Override
public OpenIsvAppDO call() throws Exception {
return openIsvAppDAO.selectByAppIdAndStatus(appId, APP_STAUTS_ACTIVE);
}
};
CacheFutureTask<OpenIsvAppDO> futureTask = new CacheFutureTask<OpenIsvAppDO>(call);
//尝试插入appId对应缓存条目
cacheFutureTask = isvAppInfos.putIfAbsent(appId,futureTask);
//如果返回为null,说明当前线程拿到了锁,可以执行查询db逻辑
if(cacheFutureTask == null) {
cacheFutureTask = futureTask;
//执行run逻辑,run最终会将操作委派到Callable的call方法
futureTask.run();
}
}
//带着超时参数获取Value信息
return cacheFutureTask.get(10, TimeUnit.SECONDS);
}catch(Exception e) {
//如果发生异常,将appId对应的FutureTask从缓存中删除
isvAppInfos.remove(appId);
}
结语
以上就是我用ConcurrentHashMap来实现本地缓存的一个例子,之所以没有用一些成熟的框架是因为我遇到的这个场景比较简单,所以选择自己动手实现,还是那句话,适合的才是最好的,做技术选型的时候要结合实际情况。
来我的公众号与我交流

juc之ConcurrentHashMap在我工作中的实践的更多相关文章
- Svn在工作中的实践感悟
Svn是一款管理项目代码的版本控制系统,是基于集中式的版本控制系统.在工作中,由于实际开发工作的需要,部门是使用Svn来管理日常的项目开发任务.使用这么长时间了,来谈谈对Svn的感悟. 首先,说下工作 ...
- 工作中常用到的Java集合类有哪些?
前言 只有光头才能变强. 文本已收录至我的GitHub精选文章,欢迎Star:https://github.com/ZhongFuCheng3y/3y Java集合是我认为在Java基础中最最重要的知 ...
- Stream流的基本介绍以及在工作中的常用操作(去重、排序以及数学运算等)
平时工作中,我在处理集合的时候,总是会用到各种流操作,但是往往在处理一些较为复杂的集合时,还是会出现无法灵活运用api的场景,这篇文章的目的,主要是为介绍一些工作中使用流时的常用操作,例如去重.排序和 ...
- 随机记录工作中常见的sql用法错误(一)
没事开始写博客,留下以前工作中常用的笔记,内容不全或者需要补充的可以留言,我只写我常用的. 网上很多类似动软生成器的小工具,这类工具虽然在表关系复杂的时候没什么软用,但是在一些简单的表结构关系还是很方 ...
- 工作中常用的js、jquery自定义扩展函数代码片段
仅记录一些我工作中常用的自定义js函数. 1.获取URL请求参数 //根据URL获取Id function GetQueryString(name) { var reg = new RegExp(&q ...
- 工作中那些提高你效率的神器(第二篇)_Listary
引言 无论是工作还是科研,我们都希望工作既快又好,然而大多数时候却迷失在繁杂的重复劳动中,久久无法摆脱繁杂的事情. 你是不是曾有这样一种想法:如果我有哆啦A梦的口袋,只要拿出神奇道具就可解当下棘手的问 ...
- 工作中那些提高你效率的神器(第一篇)_Everything
引言 无论是工作还是科研,我们都希望工作既快又好,然而大多数时候却迷失在繁杂的重复劳动中,久久无法摆脱繁杂的事情. 你是不是曾有这样一种想法:如果我有哆啦A梦的口袋,只要拿出神奇道具就可解当下棘手的问 ...
- Atitit 软件开发中 瓦哈比派的核心含义以及修行方法以及对我们生活与工作中的指导意义
Atitit 软件开发中 瓦哈比派的核心含义以及修行方法以及对我们生活与工作中的指导意义 首先我们指明,任何一种行动以及教派修行方法都有他的多元化,只看到某一方面,就不能很好的评估利弊,适不适合自己使 ...
- C# 工作中遇到的几个问题
C# 工作中遇到的几个问题 1.将VS2010中的代码编辑器的默认字体“新宋体”改为“微软雅黑”后,代码的注释,很难对齐,特别是用SandCastle Help File Builder生成帮助文档 ...
- [工作中的设计模式]解释器模式模式Interpreter
一.模式解析 解释器模式是类的行为模式.给定一个语言之后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器.客户端可以使用这个解释器来解释这个语言中的句子. 以上是解释器模式的类图,事实上我 ...
随机推荐
- 第十三届蓝桥杯大赛软件赛省赛【Java 大学B 组】试题D: 最少刷题数
1 import java.util.ArrayList; 2 import java.util.Scanner; 3 4 public class Main { 5 public static vo ...
- 关于JDK21控制台字符集编码问题
关于JDK21控制台字符集编码问题 前言: 某日尝试JDK21,idea控制台字符集编码一直乱码,后将idea所有能配置UTF-8的配置都配了一遍,无果,后搜索JDK21字符集编码相关后解决 1.配置 ...
- 【分享汇总】AIoT 开源科技节暨 OpenHarmony 技术论坛(附链接)
在开源科技 OSTech 和环球资源联手举办的"AIoT 开源科技节暨 OpenHarmony 技术论坛"上,一众技术大咖.开源鸿蒙生态上下游厂商与开发者群体齐聚一堂,畅谈&quo ...
- Go 语言 Printf 函数和格式化动词详解
Printf() 函数可以使用多种格式化动词对输出进行格式化.下面是可以与所有数据类型一起使用的一些通用格式化动词: 以下动词适用于所有数据类型: 动词 描述 %v 以默认格式打印值 %#v 以 Go ...
- 实践指南:EdgeOne与HAI的梦幻联动
在当今快速发展的数字时代,安全和速度已成为网络服务的基石.EdgeOne,作为腾讯云提供的边缘安全加速平台,以其全球部署的节点和强大的安全防护功能,为用户提供了稳定而高效的网络体验.而HAI(Hype ...
- HarmonyOS SDK,赋能开发者实现更具象、个性化开发诉求
随着移动互联网的逐步成熟,用户的需求越来越细化.鸿蒙生态为开发者提供的HarmonyOS SDK开放能力,高效赋能美团外卖等合作伙伴实现更具象.个性化的开发诉求,给用户提供更丰富便捷的体验. 点击链接 ...
- mybatis 查询一对多子表只能查出一条数据
mybatis 插叙一对多子表只能查出一条数据 环境 ssm 持久层 mybatis 关联查询一对多<collection> 原因 主表id 和子表id 一样 处理方式: select ...
- spring复习(二)AOP
1.aop基于xml以及注解的AOP配置 什么是AOP 简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强. AOP术语: ...
- 直播预告丨“Hello ArkUI:初识Slider组件(JS)”周三约起
12月1日 19:00-20:30,Hello HarmonyOS系列课程的第三节Hello ArkUI:初识Slider组件(JS)线上直播,将手把手教你熟悉最新的ArkUI,使用JS语言编写一个包 ...
- mysql 必知必会整理—sql 计算函数[六]
前言 简单整理一下sql的计算函数. 正文 函数没有SQL的可移植性强 能运行在多个系统上的代码称为可移植的(portable).相对来说,多数SQL语句是可移植的,在SQL实现之间有差异时,这些差异 ...