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在我工作中的实践的更多相关文章

  1. Svn在工作中的实践感悟

    Svn是一款管理项目代码的版本控制系统,是基于集中式的版本控制系统.在工作中,由于实际开发工作的需要,部门是使用Svn来管理日常的项目开发任务.使用这么长时间了,来谈谈对Svn的感悟. 首先,说下工作 ...

  2. 工作中常用到的Java集合类有哪些?

    前言 只有光头才能变强. 文本已收录至我的GitHub精选文章,欢迎Star:https://github.com/ZhongFuCheng3y/3y Java集合是我认为在Java基础中最最重要的知 ...

  3. Stream流的基本介绍以及在工作中的常用操作(去重、排序以及数学运算等)

    平时工作中,我在处理集合的时候,总是会用到各种流操作,但是往往在处理一些较为复杂的集合时,还是会出现无法灵活运用api的场景,这篇文章的目的,主要是为介绍一些工作中使用流时的常用操作,例如去重.排序和 ...

  4. 随机记录工作中常见的sql用法错误(一)

    没事开始写博客,留下以前工作中常用的笔记,内容不全或者需要补充的可以留言,我只写我常用的. 网上很多类似动软生成器的小工具,这类工具虽然在表关系复杂的时候没什么软用,但是在一些简单的表结构关系还是很方 ...

  5. 工作中常用的js、jquery自定义扩展函数代码片段

    仅记录一些我工作中常用的自定义js函数. 1.获取URL请求参数 //根据URL获取Id function GetQueryString(name) { var reg = new RegExp(&q ...

  6. 工作中那些提高你效率的神器(第二篇)_Listary

    引言 无论是工作还是科研,我们都希望工作既快又好,然而大多数时候却迷失在繁杂的重复劳动中,久久无法摆脱繁杂的事情. 你是不是曾有这样一种想法:如果我有哆啦A梦的口袋,只要拿出神奇道具就可解当下棘手的问 ...

  7. 工作中那些提高你效率的神器(第一篇)_Everything

    引言 无论是工作还是科研,我们都希望工作既快又好,然而大多数时候却迷失在繁杂的重复劳动中,久久无法摆脱繁杂的事情. 你是不是曾有这样一种想法:如果我有哆啦A梦的口袋,只要拿出神奇道具就可解当下棘手的问 ...

  8. Atitit 软件开发中 瓦哈比派的核心含义以及修行方法以及对我们生活与工作中的指导意义

    Atitit 软件开发中 瓦哈比派的核心含义以及修行方法以及对我们生活与工作中的指导意义 首先我们指明,任何一种行动以及教派修行方法都有他的多元化,只看到某一方面,就不能很好的评估利弊,适不适合自己使 ...

  9. C# 工作中遇到的几个问题

    C#  工作中遇到的几个问题 1.将VS2010中的代码编辑器的默认字体“新宋体”改为“微软雅黑”后,代码的注释,很难对齐,特别是用SandCastle Help File Builder生成帮助文档 ...

  10. [工作中的设计模式]解释器模式模式Interpreter

    一.模式解析 解释器模式是类的行为模式.给定一个语言之后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器.客户端可以使用这个解释器来解释这个语言中的句子. 以上是解释器模式的类图,事实上我 ...

随机推荐

  1. KingbaseES V8R6 集群运维案例 -- 集群备份到nfs共享存储初始化错误

    案例说明: 在主备库建立nfs共享存储的文件系统,作为sys_rman备份的repo-path,在备库作为repo-path节点执行备份,出现数据库连接到'5432端口的错误',数据库实际的服务端口为 ...

  2. 索引与查询使用的 collate 不一致导致无法使用索引

    索引与表的collate 不一致的情况下,会导致表上的索引不可用,这时要想使用索引,必须在SQL 语句指定建索引所用的collate. 数据库默认collate : test=# \l List of ...

  3. CVS Excell乱码怎么修改

    问题描述 日常办公,从网页或者其他web端下载的表格(excel格式或csv格式),打开后发现中文乱码,如下: 常规解法 百度会发现,大部分提供的方法,就是通过excel的另存为,然后选择相应的编码, ...

  4. PyCharm字体大小快捷键设置(“ctrl+滚轮”实现字体的随时放大和缩小)

    前言:我们在使用PyCharm工具编写Python代码的时候,希望能够随时放大缩小字体,而PyCharm默认是没有设置快捷键的,我们可以自己设置,下面就教大家如何设置. 分为两步设置: PyCharm ...

  5. 【已解决】hive导出mysql报错:Container [pid=3962,containerID=container_1632883011739_0002_01_000002] is running 270113280B beyond the 'VIRTUAL' memory limit.

    问题描述 Container [pid=3962,containerID=container_1632883011739_0002_01_000002] is running 270113280B b ...

  6. #欧拉回路,状压dp,Floyd#洛谷 6085 [JSOI2013]吃货 JYY

    题目传送门 分析 先用Floyd求出两点间的最短距离,包含必经边的欧拉回路, 先考虑欧拉回路的性质就是度数为偶数且连通,那如果有偶数个奇点可以两两配对. 设 \(g[S]\) 表示选择 \(S\) 中 ...

  7. 成长计划知识赋能 | 第十期:DAYU200开发入门秒Get

      2021年12月,OpenAtom OpenHarmony(以下简称"OpenHarmony")开源开发者成长计划开启了第一节OpenHarmony知识赋能直播课程,给广大开发 ...

  8. std::thread 五:打包任务(packaged_task)

    #include <iostream> #include <thread> #include <mutex> #include <list> #incl ...

  9. 重新整理 .net core 实践篇———承载[外篇]

    前言 简单介绍一下承载. 正文 名称叫做承载,其实就是.net core 定义的一套长期运行的服务的规范. 这个服务可以是web服务,也可以是其他服务,比如tcp,或者一些监控服务. 这里以监控服务为 ...

  10. python数据库迁移

    实际操作命令 1,python 文件.py db init 2,python xx.py db migrate -m '版本描述' 3,python xx.py db upgrade 4,python ...