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
一.模式解析 解释器模式是类的行为模式.给定一个语言之后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器.客户端可以使用这个解释器来解释这个语言中的句子. 以上是解释器模式的类图,事实上我 ...
随机推荐
- Chrome浏览器使用小技巧
前言 Notes made by IT-Pupil-Poo-Poo-Cai(IT小学生蔡坨坨). The notes are for reference only. Personal blog:www ...
- Regex中Replace方法的简单实用
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- 关于openGauss中的虚拟索引
关于 openGauss 中的虚拟索引 作为曾经的 Oracle 资深使用者,对于 Oracle 11gR2 版本推出的 invisible Index 感觉一直很良好:因为这对于大部分情况下做优化是 ...
- Prometheus 性能调优-水平分片
简介 之前笔者有连续 2 篇文章: Prometheus 性能调优 - 什么是高基数问题以及如何解决? 如何精简 Prometheus 的指标和存储占用 陆续介绍了一些 Prometheus 的性能调 ...
- HarmonyOS Connect “Device Partner”专场FAQ来啦!
原文链接:https://mp.weixin.qq.com/s/mQJlAso293qgPlA1paxv5g,点击链接查看更多技术内容: Device Partner平台是面向AIoT产业链 ...
- CentOS 6.4(64位)上安装错误libstdc++.so.6(GLIBCXX_3.4.14)解决办法
CentOS 6.4(64位)上安装错误libstdc++.so.6(GLIBCXX_3.4.14)解决办法 (2013-07-29 13:18:01) 转载▼ 分类:linux系统 引用地址: ht ...
- 在centOS上配置web服务器
centos,web服务,apache,ftp服务器,mysql,makefile (1). 检查系统是否正常 # more /var/log/messages //检查有无系统内核级错误信息 # d ...
- centos 6.4下fdisk分区、格式化、挂载新硬盘
centos 6.4下fdisk分区.格式化.挂载新硬盘 作者: cat 日期: 2013 年 9 月 10 日 发表评论 (0) 查看评论 1.# fdisk -l 查看当前磁盘信息,就会发现最下面 ...
- ArcMap分别求取矢量要素各区域的面积
本文介绍基于ArcMap软件,自动批量计算矢量图层中各个要素的面积的方法. 一次,遇到一个问题,需要分别计算ArcMap软件中一个图层的所有面要素的面积.如图,这个图层中包括多个省级行政区矢量 ...
- 部署iis7和vs2010低版本项目遇到的一些问题
前提 本人一直用iis10然后用的是vs2015,项目框架也用的是高版本的,所以后来接触了一个项目,部署iis7遇到的一些问题,按顺序总结出来,希望有所帮助. 正文 按顺序来: 1.HTTP错误 40 ...