首页 文章 精选 留言 我的

精选列表

搜索[手脑一体],共10002篇文章
优秀的个人博客,低调大师

亚马逊要求员工删除TikTok又撤回,手滑?

当地时间上周五,纽约时报报道称,亚马逊以安全风险为由要求员工将TikTok从他们的移动设备上删除。 据报道,亚马逊在上周五早上给员工的第一封电子邮件中表示:“由于安全隐患,不再允许在访问亚马逊电子邮件的移动设备上使用TikTok应用程序。 “如果您的设备上装有TikTok,则必须在7月10日之前将其删除,以保留对Amazon电子邮件的移动访问权限。目前,允许通过您的Amazon笔记本电脑浏览器使用TikTok。” 纽时记者Taylor Lorenz在推特上公布了这封电子邮件的截图。 然而反转来了,据报道,当地时间7月11日,亚马逊的一位发言人表示,电子邮件“发错了”。 亚马逊发言人说:“此前给员工的电子邮件是错误的,亚马逊给现在对TikTok的政策没有改变。” 据报道,近期,由于iOS 14中的一项新功能,TikTok被发现在后台运行时访问用户剪贴板数据,从而可能暴露密码或其他敏感数据,目前尚不清楚该行为已存在于应用程序中多久。 当地时间7月7日,美国副总统彭斯表示,因国家安全和数据隐私问题,美国将继续对TikTok等公司采取“强硬立场”。 而在前一日,美国国务卿迈克·庞培告诉福克斯新闻社 ,特朗普政府正考虑采取行动禁用TikTok等社交媒体应用。他还称,美国民众对于使用TikTok应提高警觉,不过他并没有说明美国具体将采取何种措施。 此后,TikTok否认了美国政府对其威胁国家安全和违反数据隐私的指控。 据报道,尚不完全清楚政府当局如何着手禁止该应用程序,尽管如此,故障和没安全感使TikTok社区陷入混乱状态。 TikTok发言人回应了亚马逊下令员工删除该应用程序的决定,并在声明中说:“虽然亚马逊在发送电子邮件之前未与我们通信,但我们仍然不理解他们的担忧,但我们欢迎两个公司进行对话,以便我们可以解决他们可能遇到的任何问题,并使其团队能够继续参与我们的社区。” TikTok发言人还称:“成千上万的美国人在TikTok里寻求娱乐、灵感和交流,其中包括许多奋斗在抗疫前线的亚马逊员工和承包商。我们对此感到自豪。” 据报道,TikTok是字节跳动旗下的一款短视频软件,在2016年作为国际版抖音正式进入美国市场。 市场调查公司SensorTower的数据显示,截至今年4月底,美国是TikTok在全球的第二大市场。目前,在美国,TikTok的下载量已经超过1.65亿次,约占美国总人口的50%。而美国用户平均每天打开该软件8次,每人每次平均使用的时间约为5分钟。 这么多用户中,青少年是TikTok在美国的主要用户群体。2019年,TikTok曾表示,在美国2650万的活跃用户中,约有60%的用户年龄在16-24岁。 并且,TikTok作为字节跳动独立的子公司并没有在中国运营。 2019年,字节跳动将TikTok和抖音完全分割,除了中国用户无法使用TikTok外,字节跳动在内部的技术团队也设置了防火墙,并且,工作地点在中国、工作内容为中国市场的员工,则无法获得访问TikTok关键数据的权限。 尽管如此,TikTok面临的麻烦不小,在美国已经受到了额外严格的监管,而且最近确实因为一些举动引起了很大争议。 例如,硅星人报道TikTok滥用iOS剪贴板权限,在用户不知情的情况下频繁访问剪切板,因此也成为了认为TikTok在侵犯隐私的证据之一。不过对此,TikTok表示这是为了“反垃圾账号”,并且在事情曝光后停止了这一做法。 不仅如此,美国军方已经禁止士兵安装这款应用,而美国监督机构一直“不信任”TikTok。 自2019年起,美国监督机构加大了对 TikTok的审查力度,审查的内容主要涉及威胁国家安全和违反数据隐私两方面。 例如2019年2月,美国联邦贸易委员会(FTC)起诉TikTok,FTC表示,TikTok未经父母同意非法收集了13岁以下儿童的个人信息。对此,FTC给TikTok开出了570万美元的罚单。 针对美国政府的多次指控,TikTok通过设立透明中心、分离国内外市场、聘请本土高管等行为尝试从多方面树立维护数据隐私的形象。 并且2020年3月,TikTok在美国洛杉矶设立了透明中心,还计划在华盛顿开设第二个透明中心。 不光美国,当印度禁止使用TikTok后,通过该平台工作的印度网红们损失惨重,例如“鸡腿哥”。 据华盛顿邮报当地时间7月11日报道,来自印度的41岁健身房老板卡玛什,此前在TikTok上分享吃鸡腿而闻名,短短几个月内,他就吸引了近全球700万粉丝。 印度是TikTok最大的用户市场。如今,不少印度大V用户郁闷地表示,禁令让他们失去了主要的收入来源,现在,他们必须找到一个新的平台。 本文转自雷锋网,如需转载请至雷锋网官网申请授权。

优秀的个人博客,低调大师

集成支撑平台:智慧城市隐患的“扫雷手”

21世纪以来的15年是我国城镇化快速发展的15年,快速发展中也出现了一些矛盾和冲突,如资源短缺、交通拥堵、环境污染、安全隐患等,而发展智慧城市是解决这些新问题的一个重要手段。 近年来,我国智慧城市在国家政策的推动下有了较为长足的发展,如智慧交通、智慧医疗、互联网+、大数据、云计算等。现如今,人们只要在家里动动手,上网一查询,就可以实时了解道路及公交信息;只要手机一操作,就可以实现看病在线预约挂号,甚至在线问诊或远程诊疗;即便是政务问题,也可以通过网络在“家里”解决。这样类似的还有很多便民利民的举措均得益于智慧城市的推行。 不过,你可曾知道,就这随手一点的便捷,都是基于对各个领域海量数据信息的收集、分析及处理等环节的支撑。而因智慧城市产生的海量信息在处理分析过程中却存在“信息孤岛”等现象,因此急需一个大的平台来打通各领域的信息通道,为社会提供更加高效、便捷、多样化的服务,扫除城市隐患。正元地理信息有限责任公司智慧城市集成支撑平台即致力于探索解决相关问题。 集成支撑平台或为解决“信息孤岛”一剂良药 智慧城市建设的目标是感知、物联、智能。行业人士对智慧城市的定义认为,在城市发展中,数字城市是智慧城市的萌芽阶段,主要通过对城市各领域数据的采集为未来奠定基础;第二阶段为智能城市,该阶段主要致力于政务智能,使为民服务与监管更加便捷;而智慧城市则是前两个阶段的升级,在大数据收集基础上,打通各部门间通道,使信息数据交互融合,从而更好地服务于民。 例如某人想要申请公司注册,通过智慧政务,一个部门审批完后通过电子方式流转到下一部门继续审批。在智慧城市中,不仅可以进行电子审批,并且通过机构流程再造,还可将同时审批的部门设置为并行,进一步提高审批效率。一些可以网上申请审批的事项,可通过手机申请,也可通过社区门口的政务屏申请。需要提交资料的,可提前在市民大厅预订一个资料柜,通过二维码进行资料投放,免去舟车劳顿排队良久只为递件的麻烦。 当然,这只是一个美好的愿景,虽然一些城市或地区已着手开始尝试,但在一些基础性工作中,还有不少问题需要解决。例如智慧城市发展中出现的“信息孤岛”现象。 正元地理信息有限责任公司研究开发中心基础研发部副经理胡杰在接受中国经济导报记者采访时表示,“智慧城市建设发展迅速,交通、市政、社区等领域都取得了一定成绩,但智慧应用之间业务、数据不共享问题日益突出,在处理问题时经常难以形成闭环。而这些智慧应用中往往存在着一些可以共享共用的数据、软件或硬件设施,这就导致了使用的不便和重复建设的浪费。”城市信息化的发展对城市信息化的网络基础设施建设、信息资源数据库建设和共享、城市管理与运行相关系统功能提升等都提出了新的要求,迫切需要解决如下问题:城市“感知”节点远远不够,无法满足精细化管理需要;城市 各部门业务系统呈“信息孤岛”态势,跨部门协同能力较弱;城市管理海量数据处理和分析能力不足,无法满足城市管理综合监控和智能化决策的需要等。因此,需要通过新的视角、新的思路、新的技术手段和更加全面系统的方法来加以解决和实现。 于是,去年12月24日,国务院发布《关于深入推进城市执法体制改革改进城市管理工作的指导意见》,其中指出,要综合设置机构、整合信息平台。“在这一文件发布之前,正元已于去年7月开始了智慧城市集成支撑平台的研发。现在看来,正元建设智慧城市的思路很好地响应了这一文件的要求。”胡杰自豪地说道,通过智慧城市集成支撑平台,可对数据、业务等进行整合、利用,从而用以支撑智慧城市的建设。 胡杰向中国经济导报记者介绍,智慧应用建设过程中出现的“信息孤岛”现象,无法将信息资源统一起来做相关应用、分析,无法为智慧城市整体的决策进行支持。而集成支撑平台可整合智慧城市各方面信息,进而可提供更为智能的决策,且因为集成的功能,可避免重复建设的浪费。“我们首先会建立大数据中心,将人口基础信息、法人单位基础信息、自然资源和空间地理基础信息、宏观经济信息以及物联网数据、市政、人防、国土、公安等行业数据统一融合纳入。进而,能够根据上层应用的需要来使用这些数据。平台提供了完备的接口,以服务的形式对外共享数据,并将外部数据集成进来,通过抽取、清洗等一系列的步骤保存在数据中心。”胡杰进一步解释道。 此外,以服务形式提供的除了数据还有功能,如地理信息服务、商业智能服务、数据挖掘服务、物联网服务、业务专项服务等应用服务,日志、缓存、配置、身份认证等通用服务都包含在内。 平台可在消除城市隐患等更为广泛的场景中发挥作用 胡杰告诉中国经济导报记者,集成支撑平台的意义在于整合了较为全面的数据,并结合“云”的思想,以平台层的身份对外提供服务支撑。“只有掌握了更全面的数据,才能从全局进行决策分析,比如在处理应急事件时,需要人防、安检、消防等各部门的数据,而目前各部门间信息数据的通道仍不是很畅通。另一方面,整合数据、统一对外提供服务避免了重复建设。” 可以说,对于在地理信息领域浸淫已久的正元而言,早已建成多年的地理信息共享服务平台是智慧城市集成支撑平台的一个重要基础,亦或说现在的支撑平台是其升华。“不但在业务、数据方面不再局限于地理信息行业,更是通过云计算、大数据等一系列新技术的运用满足了性能、功能上的需求。平台层应用的清晰定位决定了它能够在更广泛的场景中得到使用。当然,地理信息尤其是管线方面的优势,在我们这个平台中也得到了继承和发扬。” 据悉,目前已有数家公司建成了智慧城市整体运营类产品。这些产品均能对市政、交通、环保等行业进行一定程度的支撑,但以单独系统存在的形式使其在集成和共享数据、服务,以及整合第三方应用方面有着天生的短板;此外,在被称为“城市生命线”的地下管线管理工作中,因为缺少技术储备和行业积累,很难实现有效的监管。而正元智慧城市集成支撑平台在履行为其他系统提供数据、功能服务使命的同时,很好地支持了公司在数十年来逐渐形成的一套地下管线“全生命周期”管理办法,在满足常规规划设计、建设施工、运行维护、报废处置工作需要的基础上,能够有效预防、及时发现、合理处置地下管线隐患、事故,最大限度避免人身、财产损失,可更好地保障公共安全。 据胡杰透露,借力集团内事业部、各分子公司的鼎力支持,目前如此“包容”的一个平台主体功能已研发完毕,该平台已在智慧城市项目中成功实施。“它集中了原本分散在不同部门的各类数据,为城市安全预警、城市应急指挥、城市领导指挥决策等城市整体管理工作,以及供水科学调度、防汛排涝、绿色照明等专项业务应用提供了强有力的支撑。”胡杰说,“以城市应急来说,在集成支撑平台的帮助下,我们能够及时获取事故周边的危险源、人口密集场所、医疗力量、紧急避难场所、救援人员物资等各类信息,通过路径分析等服务可获取人员撤离、救援的最佳路径,通过大数据手段预判可能造成的次生事故,从而进行预防和监控,避免事态扩大。” 不过,胡杰强调,在实施过程中,集成整合数据其实是一件十分困难的工作。由于历史原因,各部门间存在着数据壁垒,不仅各自的数据储存于不同场所,这些数据的种类、结构、管理方法也不尽相同。因此,建立标准规范对于数据整合的意义非同小可。在此之后,只有经过数据抽取、清洗和转换,这一系列多样繁杂的数据才能为我所用。 数据的挖掘及提升运算性能是持续性任务 在智慧城市中,大数据的角色愈发重要。如何有效挖掘、分析大数据,是准确高效解决城市运营问题的关键。“例如城市安全预警需建立模型,设计人员只能先通过经验或推理来假设某一城市领域在何种条件下将会产生哪种问题,但实际上还需要挖掘大量数据来分析验证推理条件的合理性并提高其准确性。但由于受数据积累时间不够长,且一些数据不易协调等客观因素影响,暂时无法在短期内做到十分完善。”胡杰坦言。 下一步,正元将继续深化大数据挖掘,充分利用云计算的能力提升性能。“平台虽然已经建立,但部分功能在技术方面仍有改进的空间。”胡杰解释,“如管线安全评价服务,目前仍通过传统的方式实现,如需对较大区域进行分析耗时会比较长,但运用大数据技术,则能够大大缩短时间。”有时城市数据分析需要做到秒级反应,如遇到应急情况,需在最短时间内迅速计算并反馈,此时传统运算方式的速度远不能及,这就需要大数据、云计算来发挥作用。 值得一提的是,数据已成为国家基础性战略资源。今年9月,《促进大数据发展行动纲要》明确指出,在未来5~10年内,要打造精准治理、多方协作的社会治理新模式,要加快政府数据开放、共享,要推进数据资源的整合及治理能力的提升。胡杰希望,利国利民的政策一定要快速落地实现。 本文转自d1net(转载)

优秀的个人博客,低调大师

(附脑图)

运维开发这个岗位与普通的业务开发不同,与日常的运维工作也不同。要求兼顾开发与运维两种能力。既要掌握不弱于业务开发的开发技术;又要负责SRE同学日常的运维能力;上线之前,还要像QA同学一样,对自己的服务进行测试和分级变更。 多种能力的交叉,造就不一样的视角:这群人给自己起了一个很简约的名字:DevOps。 DevOps 按百度百科解释:DevOps是开发、技术运营和质量保障三者的交集。在我看来,DevOps其实只是一种方法论,从这种综合的视角出发,包含一些基本原则和实践方法,仅此而已。 DevOps从架构、开发、测试、发布、运维、变更整个流程来考量,从这种综合的视角出发,能将部门之间的沟通隔阂消灭于无形,会给我们公司和项目注入新的活力。 DevOps这个概念,本文暂不做讨论,本文内容只针对运维领域自动化平台开发的工作进行探讨。 一、前言 运维开

优秀的个人博客,低调大师

真香,手撸的 SpringBoot缓存系统,性能杠杠的!

缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。 本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统。 一、通用缓存接口 1、缓存基础算法 FIFO(First In First Out),先进先出,和OS里的FIFO思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。 LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。 LRU(Least Recently Used),最近最少使用,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据移除。 2、接口定义 简单定义缓存接口,大致可以抽象如下: packagecom.power.demo.cache.contract;importjava.util.function.Function;/***缓存提供者接口**/publicinterfaceCacheProviderService{/***查询缓存**@paramkey缓存键不可为空**/<TextendsObject>Tget(Stringkey);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/<TextendsObject>Tget(Stringkey,Function<String,T>function);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime);/***设置缓存键值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/<TextendsObject>voidset(Stringkey,Tobj);/***设置缓存键值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime);/***移除缓存**@paramkey缓存键不可为空**/voidremove(Stringkey);/***是否存在缓存**@paramkey缓存键不可为空**/booleancontains(Stringkey);} 注意,这里列出的只是常见缓存功能接口,一些在特殊场景下用到的统计类的接口、分布式锁、自增(减)等功能不在讨论范围之内。 Get相关方法,注意多个参数的情况,缓存接口里面传人的Function,这是Java8提供的函数式接口,虽然支持的入参个数有限(这里你会非常怀念.NET下的Func委托),但是仅对Java这个语言来说,这真是一个重大的进步^_^。 接口定义好了,下面就要实现缓存提供者程序了。按照存储类型的不同,本文简单实现最常用的两种缓存提供者:本地缓存和分布式缓存。 二、本地缓存 本地缓存,也就是JVM级别的缓存(本地缓存可以认为是直接在进程内通信调用,而分布式缓存则需要通过网络进行跨进程通信调用),一般有很多种实现方式,比如直接使用Hashtable、ConcurrentHashMap等天生线程安全的集合作为缓存容器,或者使用一些成熟的开源组件,如EhCache、Guava Cache等。本文选择上手简单的Guava缓存。 1、什么是Guava Guava,简单来说就是一个开发类库,且是一个非常丰富强大的开发工具包,号称可以让使用Java语言更令人愉悦,主要包括基本工具类库和接口、缓存、发布订阅风格的事件总线等。在实际开发中,我用的最多的是集合、缓存和常用类型帮助类,很多人都对这个类库称赞有加。 2、添加依赖 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency> 3、实现接口 /**本地缓存提供者服务(GuavaCache)**/@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)@Qualifier("localCacheService")publicclassLocalCacheProviderImplimplementsCacheProviderService{privatestaticMap<String,Cache<String,Object>>_cacheMap=Maps.newConcurrentMap();static{Cache<String,Object>cacheContainer=CacheBuilder.newBuilder().maximumSize(AppConst.CACHE_MAXIMUM_SIZE).expireAfterWrite(AppConst.CACHE_MINUTE,TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次访问后的一段时间移出.recordStats()//开启统计功能.build();_cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE),cacheContainer);}/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=get(key,null,null,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=get(key,function,key,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=get(key,function,funcParm,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime){Tobj=get(key,function,key,expireTime);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime){Tobj=null;if(StringUtils.isEmpty(key)==true){returnobj;}expireTime=getExpireTime(expireTime);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);try{if(function==null){obj=(T)cacheContainer.getIfPresent(key);}else{finalLongcachedTime=expireTime;obj=(T)cacheContainer.get(key,()->{TretObj=function.apply(funcParm);returnretObj;});}}catch(Exceptione){e.printStackTrace();}returnobj;}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){set(key,obj,AppConst.CACHE_MINUTE);}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){if(StringUtils.isEmpty(key)==true){return;}if(obj==null){return;}expireTime=getExpireTime(expireTime);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);cacheContainer.put(key,obj);}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){if(StringUtils.isEmpty(key)==true){return;}longexpireTime=getExpireTime(AppConst.CACHE_MINUTE);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);cacheContainer.invalidate(key);}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}privatestaticLocklock=newReentrantLock();privateCache<String,Object>getCacheContainer(LongexpireTime){Cache<String,Object>cacheContainer=null;if(expireTime==null){returncacheContainer;}StringmapKey=String.valueOf(expireTime);if(_cacheMap.containsKey(mapKey)==true){cacheContainer=_cacheMap.get(mapKey);returncacheContainer;}try{lock.lock();cacheContainer=CacheBuilder.newBuilder().maximumSize(AppConst.CACHE_MAXIMUM_SIZE).expireAfterWrite(expireTime,TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次访问后的一段时间移出.recordStats()//开启统计功能.build();_cacheMap.put(mapKey,cacheContainer);}finally{lock.unlock();}returncacheContainer;}/***获取过期时间单位:毫秒**@paramexpireTime传人的过期时间单位毫秒如小于1分钟,默认为10分钟**/privateLonggetExpireTime(LongexpireTime){Longresult=expireTime;if(expireTime==null||expireTime<AppConst.CACHE_MINUTE/10){result=AppConst.CACHE_MINUTE;}returnresult;}} 4、注意事项 Guava Cache初始化容器时,支持缓存过期策略,类似FIFO、LRU和LFU等算法。 expireAfterWrite:最后一次写入后的一段时间移出。 expireAfterAccess:最后一次访问后的一段时间移出。 Guava Cache对缓存过期时间的设置实在不够友好。常见的应用场景,比如,有些几乎不变的基础数据缓存1天,有些热点数据缓存2小时,有些会话数据缓存5分钟等等。 通常我们认为设置缓存的时候带上缓存的过期时间是非常容易的,而且只要一个缓存容器实例即可,比如.NET下的ObjectCache、System.Runtime.Cache等等。 但是Guava Cache不是这个实现思路,如果缓存的过期时间不同,Guava的CacheBuilder要初始化多份Cache实例。 好在我在实现的时候注意到了这个问题,并且提供了解决方案,可以看到getCacheContainer这个函数,根据过期时长做缓存实例判断,就算不同过期时间的多实例缓存也是完全没有问题的。 三、分布式缓存 分布式缓存产品非常多,本文使用应用普遍的Redis,在Spring Boot应用中使用Redis非常简单。 1、什么是Redis Redis是一款开源(BSD许可)的、用C语言写成的高性能的键-值存储(key-value store)。它常被称作是一款数据结构服务器(data structure server)。它可以被用作缓存、消息中间件和数据库,在很多应用中,经常看到有人选择使用Redis做缓存,实现分布式锁和分布式Session等。作为缓存系统时,和经典的KV结构的Memcached非常相似,但又有很多不同。 Redis支持丰富的数据类型。Redis的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等数据类型。对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。 Redis的数据类型: Keys:非二进制安全的字符类型( not binary-safe strings ),由于key不是binary safe的字符串,所以像“my key”和“mykey\n”这样包含空格和换行的key是不允许的。Values:Strings、Hash、Lists、 Sets、 Sorted sets。考虑到Redis单线程操作模式,Value的粒度不应该过大,缓存的值越大,越容易造成阻塞和排队。 为了获得优异的性能,Redis采用了内存中(in-memory)数据集(dataset)的方式。同时,Redis支持数据的持久化,你可以每隔一段时间将数据集转存到磁盘上(snapshot),或者在日志尾部追加每一条操作命令(append only file,aof)。 Redis同样支持主从复制(master-slave replication),并且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、网络断开自动重连等功能。 同时Redis还具有其它一些特性,其中包括简单的事物支持、发布订阅 ( pub/sub)、管道(pipeline)和虚拟内存(vm)等 。 2、添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency> 3、配置Redis 在application.properties配置文件中,配置Redis常用参数: ##Redis缓存相关配置#Redis数据库索引(默认为0)spring.redis.database=0#Redis服务器地址spring.redis.host=127.0.0.1#Redis服务器端口spring.redis.port=6379#Redis服务器密码(默认为空)spring.redis.password=123321#Redis连接超时时间默认:5分钟(单位:毫秒)spring.redis.timeout=300000ms#Redis连接池最大连接数(使用负值表示没有限制)spring.redis.jedis.pool.max-active=512#Redis连接池中的最小空闲连接spring.redis.jedis.pool.min-idle=0#Redis连接池中的最大空闲连接spring.redis.jedis.pool.max-idle=8#Redis连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.jedis.pool.max-wait=-1ms 常见的需要注意的是最大连接数(spring.redis.jedis.pool.max-active )和超时时间(spring.redis.jedis.pool.max-wait)。Redis在生产环境中出现故障的频率经常和这两个参数息息相关。 接着定义一个继承自CachingConfigurerSupport(请注意cacheManager和keyGenerator这两个方法在子类的实现)的RedisConfig类: packagecom.power.demo.cache.config;importorg.springframework.cache.CacheManager;importorg.springframework.cache.annotation.CachingConfigurerSupport;importorg.springframework.cache.annotation.EnableCaching;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.cache.RedisCacheManager;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.StringRedisSerializer;/***Redis缓存配置类*/@Configuration@EnableCachingpublicclassRedisConfigextendsCachingConfigurerSupport{@BeanpublicCacheManagercacheManager(RedisConnectionFactoryconnectionFactory){returnRedisCacheManager.create(connectionFactory);}@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryfactory){RedisTemplate<String,Object>template=newRedisTemplate<>();//Jedis的Key和Value的序列化器默认值是JdkSerializationRedisSerializer//经实验,JdkSerializationRedisSerializer通过RedisDesktopManager看到的键值对不能正常解析//设置key的序列化器template.setKeySerializer(newStringRedisSerializer());////设置value的序列化器默认值是JdkSerializationRedisSerializer//使用Jackson序列化器的问题是,复杂对象可能序列化失败,比如JodaTime的DateTime类型////使用Jackson2,将对象序列化为JSON//Jackson2JsonRedisSerializerjackson2JsonRedisSerializer=newJackson2JsonRedisSerializer(Object.class);////json转对象类,不设置默认的会将json转成hashmap//ObjectMapperom=newObjectMapper();//om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);//jackson2JsonRedisSerializer.setObjectMapper(om);//template.setValueSerializer(jackson2JsonRedisSerializer);//将redis连接工厂设置到模板类中template.setConnectionFactory(factory);returntemplate;}////自定义缓存key生成策略//@Bean//publicKeyGeneratorkeyGenerator(){//returnnewKeyGenerator(){//@Override//publicObjectgenerate(Objecttarget,java.lang.reflect.Methodmethod,Object...params){//StringBuffersb=newStringBuffer();//sb.append(target.getClass().getName());//sb.append(method.getName());//for(Objectobj:params){//if(obj==null){//continue;//}//sb.append(obj.toString());//}//returnsb.toString();//}//};//}} 在RedisConfig这个类上加上@EnableCaching这个注解,这个注解会被Spring发现,并且会创建一个切面(aspect) 并触发Spring缓存注解的切点(pointcut)。据所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值。 cacheManager方法,申明一个缓存管理器(CacheManager)的bean,作用就是@EnableCaching这个切面在新增缓存或者删除缓存的时候会调用这个缓存管理器的方法。keyGenerator方法,可以根据需求自定义缓存key生成策略。 而redisTemplate方法,则主要是设置Redis模板类,比如键和值的序列化器(从这里可以看出,Redis的键值对必须可序列化)、redis连接工厂等。 RedisTemplate支持的序列化器主要有如下几种: JdkSerializationRedisSerializer:使用Java序列化; StringRedisSerializer:序列化String类型的key和value; GenericToStringSerializer:使用Spring转换服务进行序列化; JacksonJsonRedisSerializer:使用Jackson 1,将对象序列化为JSON; Jackson2JsonRedisSerializer:使用Jackson 2,将对象序列化为JSON; OxmSerializer:使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实现序列化,用于XML序列化; 注意:RedisTemplate的键和值序列化器,默认情况下都是JdkSerializationRedisSerializer,它们都可以自定义设置序列化器。 推荐将字符串键使用StringRedisSerializer序列化器,因为运维的时候好排查问题,JDK序列化器的也能识别,但是可读性稍差(是因为缓存服务器没有JRE吗?),见如下效果: 而值序列化器则要复杂的多,很多人推荐使用Jackson2JsonRedisSerializer序列化器,但是实际开发过程中,经常有人碰到反序列化错误,经过排查多数都和Jackson2JsonRedisSerializer这个序列化器有关。 4、实现接口 使用RedisTemplate,在Spring Boot中调用Redis接口比直接调用Jedis简单多了。 packagecom.power.demo.cache.impl;importcom.power.demo.cache.contract.CacheProviderService;importcom.power.demo.common.AppConst;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.ValueOperations;importorg.springframework.util.StringUtils;importjavax.annotation.Resource;importjava.io.Serializable;importjava.util.concurrent.TimeUnit;importjava.util.function.Function;@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)@Qualifier("redisCacheService")publicclassRedisCacheProviderImplimplementsCacheProviderService{@ResourceprivateRedisTemplate<Serializable,Object>redisTemplate;/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=get(key,null,null,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=get(key,function,key,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=get(key,function,funcParm,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime){Tobj=get(key,function,key,expireTime);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime){Tobj=null;if(StringUtils.isEmpty(key)==true){returnobj;}expireTime=getExpireTime(expireTime);try{ValueOperations<Serializable,Object>operations=redisTemplate.opsForValue();obj=(T)operations.get(key);if(function!=null&&obj==null){obj=function.apply(funcParm);if(obj!=null){set(key,obj,expireTime);//设置缓存信息}}}catch(Exceptione){e.printStackTrace();}returnobj;}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){set(key,obj,AppConst.CACHE_MINUTE);}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){if(StringUtils.isEmpty(key)==true){return;}if(obj==null){return;}expireTime=getExpireTime(expireTime);ValueOperations<Serializable,Object>operations=redisTemplate.opsForValue();operations.set(key,obj);redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){if(StringUtils.isEmpty(key)==true){return;}redisTemplate.delete(key);}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}/***获取过期时间单位:毫秒**@paramexpireTime传人的过期时间单位毫秒如小于1分钟,默认为10分钟**/privateLonggetExpireTime(LongexpireTime){Longresult=expireTime;if(expireTime==null||expireTime<AppConst.CACHE_MINUTE/10){result=AppConst.CACHE_MINUTE;}returnresult;}} 注意:很多教程里都讲到通过注解的方式(@Cacheable,@CachePut、@CacheEvict和@Caching)实现数据缓存,根据实践,我个人是不推崇这种使用方式的。 四、缓存“及时”过期问题 这个也是开发和运维过程中非常经典的问题。 有些公司写缓存客户端的时候,会给每个团队分别定义一个Area,但是这个只能做到缓存键的分布区分,不能保证缓存“实时”有效的过期。 多年以前我写过一篇结合实际情况的文章,也就是加上缓存版本,请猛击这里 ,算是提供了一种相对有效的方案,不过高并发站点要慎重,防止发生雪崩效应。 Redis还有一些其他常见问题,比如:Redis的字符串类型Key和Value都有限制,且都是不能超过512M,请猛击这里。还有最大连接数和超时时间设置等问题,本文就不再一一列举了。 五、二级缓存 在配置文件中,加上缓存提供者开关: ##是否启用本地缓存spring.power.isuselocalcache=1##是否启用Redis缓存spring.power.isuserediscache=1 缓存提供者程序都实现好了,我们会再包装一个调用外观类PowerCacheBuilder,加上缓存版本控制,可以轻松自如地控制和切换缓存,code talks: packagecom.power.demo.cache;importcom.google.common.collect.Lists;importcom.power.demo.cache.contract.CacheProviderService;importcom.power.demo.common.AppConst;importcom.power.demo.common.AppField;importcom.power.demo.util.ConfigUtil;importcom.power.demo.util.PowerLogger;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importjava.util.List;importjava.util.UUID;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;importjava.util.function.Function;/**支持多缓存提供程序多级缓存的缓存帮助类**/@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)publicclassPowerCacheBuilder{@Autowired@Qualifier("localCacheService")privateCacheProviderServicelocalCacheService;@Autowired@Qualifier("redisCacheService")privateCacheProviderServiceredisCacheService;privatestaticList<CacheProviderService>_listCacheProvider=Lists.newArrayList();privatestaticfinalLockproviderLock=newReentrantLock();/***初始化缓存提供者默认优先级:先本地缓存,后分布式缓存**/privateList<CacheProviderService>getCacheProviders(){if(_listCacheProvider.size()>0){return_listCacheProvider;}//线程安全try{providerLock.tryLock(1000,TimeUnit.MILLISECONDS);if(_listCacheProvider.size()>0){return_listCacheProvider;}StringisUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);CacheProviderServicecacheProviderService=null;//启用本地缓存if("1".equalsIgnoreCase(isUseCache)){_listCacheProvider.add(localCacheService);}isUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);//启用Redis缓存if("1".equalsIgnoreCase(isUseCache)){_listCacheProvider.add(redisCacheService);resetCacheVersion();//设置分布式缓存版本号}PowerLogger.info("初始化缓存提供者成功,共有"+_listCacheProvider.size()+"个");}catch(Exceptione){e.printStackTrace();_listCacheProvider=Lists.newArrayList();PowerLogger.error("初始化缓存提供者发生异常:{}",e);}finally{providerLock.unlock();}return_listCacheProvider;}/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=null;//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){obj=provider.get(key);if(obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,funcParm);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,funcParm);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,longexpireTime){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,expireTime);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,expireTime);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,longexpireTime){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,funcParm,expireTime);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,funcParm,expireTime);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***设置缓存键值直接向缓存中插入或覆盖值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){provider.set(key,obj);}}/***设置缓存键值直接向缓存中插入或覆盖值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){provider.set(key,obj,expireTime);}}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){//key=generateVerKey(key);//构造带版本的缓存键if(StringUtils.isEmpty(key)==true){return;}for(CacheProviderServiceprovider:getCacheProviders()){provider.remove(key);}}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;//key=generateVerKey(key);//构造带版本的缓存键if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}/***获取分布式缓存版本号**/publicStringgetCacheVersion(){Stringversion="";booleanisUseCache=checkUseRedisCache();//未启用Redis缓存if(isUseCache==false){returnversion;}version=redisCacheService.get(AppConst.CACHE_VERSION_KEY);returnversion;}/***重置分布式缓存版本如果启用分布式缓存,设置缓存版本**/publicStringresetCacheVersion(){Stringversion="";booleanisUseCache=checkUseRedisCache();//未启用Redis缓存if(isUseCache==false){returnversion;}//设置缓存版本version=String.valueOf(Math.abs(UUID.randomUUID().hashCode()));redisCacheService.set(AppConst.CACHE_VERSION_KEY,version);returnversion;}/***如果启用分布式缓存,获取缓存版本,重置查询的缓存key,可以实现相对实时的缓存过期控制*<p>*如没有启用分布式缓存,缓存key不做修改,直接返回**/publicStringgenerateVerKey(Stringkey){Stringresult=key;if(StringUtils.isEmpty(key)==true){returnresult;}booleanisUseCache=checkUseRedisCache();//没有启用分布式缓存,缓存key不做修改,直接返回if(isUseCache==false){returnresult;}Stringversion=redisCacheService.get(AppConst.CACHE_VERSION_KEY);if(StringUtils.isEmpty(version)==true){returnresult;}result=String.format("%s_%s",result,version);returnresult;}/***验证是否启用分布式缓存**/privatebooleancheckUseRedisCache(){booleanisUseCache=false;StringstrIsUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);isUseCache="1".equalsIgnoreCase(strIsUseCache);returnisUseCache;}} 单元测试如下: @TestpublicvoidtestCacheVerson()throwsException{Stringversion=cacheBuilder.getCacheVersion();System.out.println(String.format("当前缓存版本:%s",version));StringcacheKey=cacheBuilder.generateVerKey("goods778899");GoodsVOgoodsVO=newGoodsVO();goodsVO.setGoodsId(UUID.randomUUID().toString());goodsVO.setCreateTime(newDate());goodsVO.setCreateDate(newDateTime(newDate()));goodsVO.setGoodsType(1024);goodsVO.setGoodsCode("123456789");goodsVO.setGoodsName("我的测试商品");cacheBuilder.set(cacheKey,goodsVO);GoodsVOgoodsVO1=cacheBuilder.get(cacheKey);Assert.assertNotNull(goodsVO1);version=cacheBuilder.resetCacheVersion();System.out.println(String.format("重置后的缓存版本:%s",version));cacheKey=cacheBuilder.generateVerKey("goods112233");cacheBuilder.set(cacheKey,goodsVO);GoodsVOgoodsVO2=cacheBuilder.get(cacheKey);Assert.assertNotNull(goodsVO2);Assert.assertTrue("两个缓存对象的主键相同",goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId()));} 一个满足基本功能的多级缓存系统就好了。 在Spring Boot应用中使用缓存则非常简洁,选择调用上面包装好的缓存接口即可。 StringcacheKey=_cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid."+request.getGoodsId());GoodsVOgoodsVO=_cacheBuilder.get(cacheKey,_goodsService::getGoodsByGoodsId,request.getGoodsId()); 到这里Spring Boot业务系统开发中最常用到的ORM,缓存和队列三板斧就介绍完了。 在开发的过程中你会发现,Java真的是非常非常中规中矩的语言,你需要不断折腾并熟悉常见的开源中间件和工具,开源的轮子实在是太丰富,多尝试几个,实践出真知。 作者:Jeff Wong来源:cnblogs.com/jeffwongishandsome -END- 1.3 万亿条数据查询,如何做到毫秒级响应? 分享六个经典的SpringBoot开源项目 一套通用的后台管理系统,赚钱就靠它了! SpringBoot 开发案例之接入腾讯云短信 推荐一款基于 Java 的身份证号码识别系统 分享一个支付大屏实时监控数据平台 推荐一款清爽的实时监控大屏附安装教程 微信支付收银台功能上线了 支付宝支付新版 SDK 上线,让支付触手可及 太厉害了!我用 Nginx 提升系统10倍性能 本文分享自微信公众号 - 爪哇笔记(Java_notes)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

为了搞清楚类加载,竟然手撸JVM!

持续坚持原创输出,点击蓝字关注我吧 作者:小傅哥博客:https://bugstack.cn ❝ 沉淀、分享、成长,让自己和他人都能有所收获!😜 ❞ 目录 一、前言 二、面试题 三、类加载过程描述 四、写个代码加载下 1. 案例工程 2. 代码讲解 五、解析字节码文件 1. 提取部分字节码 2. 解析魔数并校验 3. 解析版本号信息 4. 解析全部内容对照 六、总结 七、系列推荐 一、前言 学习,不知道从哪下手? 当学习一个新知识不知道从哪下手的时候,最有效的办法是梳理这个知识结构的脉络信息,汇总出一整张的思维导出。接下来就是按照思维导图的知识结构,一个个学习相应的知识点,并汇总记录。 就像JVM的学习,可以说它包括了非常多的内容,也是一个庞大的知识体系。例如:类加载、加载器、生命周期、性能优化、调优参数、调优工具、优化方案、内存区域、虚拟机栈、直接内存、内存溢出、元空间、垃圾回收、可达性分析、标记清除、回收过程等等。如果没有梳理的一头扎进去,东一榔头西一棒子,很容易造成学习恐惧感。 如图 24-1 是 JVM 知识框架梳理,后续我们会按照这个结构陆续讲解每一块内容。 图 24-1 JVM 知识框架 二、面试题 谢飞机,小记!,很多知识根本就是背背背,也没法操作,难学! 「谢飞机」:大哥,你问我两个JVM问题,我看看我自己还行不! 「面试官」:啊?嗯!往死了问还是? 「谢飞机」:就就就,都行!你看着来! 「面试官」:啊,那 JVM 加载过程都是什么步骤? 「谢飞机」:巴拉巴拉,加载、验证、准备、解析、初始化、使用、卸载! 「面试官」:嗯,背的挺好!我怀疑你没操作过! 那加载的时候,JVM 规范规定从第几位开始是解析常量池,以及数据类型是如何定义的,u1、u2、u4,是怎么个玩意? 「谢飞机」:握草!算了,告诉我看啥吧! 三、类加载过程描述 图 24-2 JVM 类加载过程 「JVM 类加载过程分为」,加载、链接、初始化、使用和卸载这四个阶段,在链接中又包括:验证、准备、解析。 「加载」:Java 虚拟机规范对 class 文件格式进行了严格的规则,但对于从哪里加载 class 文件,却非常自由。Java 虚拟机实现可以从文件系统读取、从JAR(或ZIP)压缩包中提取 class 文件。除此之外也可以通过网络下载、数据库加载,甚至是运行时直接生成的 class 文件。 「链接」:包括了三个阶段; 验证,确保被加载类的正确性,验证字节流是否符合 class 文件规范,例魔数 0xCAFEBABE,以及版本号等。 准备,为类的静态变量分配内存并设置变量初始值等 解析,解析包括解析出常量池数据和属性表信息,这里会包括 ConstantPool 结构体以及 AttributeInfo 接口等。 「初始化」:类加载完成的最后一步就是初始化,目的就是为标记常量值的字段赋值,以及执行 <clinit> 方法的过程。 JVM虚拟机通过锁的方式确保 clinit 仅被执行一次 「使用」:程序代码执行使用阶段。 「卸载」:程序代码退出、异常、结束等。 四、写个代码加载下 JVM 之所以不好掌握,主要是因为不好实操。虚拟机是 C++ 写的,很多 Java 程序员根本就不会去读,或者读不懂。那么,也就没办法实实在在的体会到,到底是怎么加载的,加载的时候都干了啥。只有看到代码,我才觉得自己学会了! 所以,我们这里要手动写一下,JVM 虚拟机的部分代码,也就是类加载的过程。通过 Java 代码来实现 Java 虚拟机的部分功能,让开发 Java 代码的程序员更容易理解虚拟机的执行过程。 1. 案例工程 interview-24├──pom.xml└──src└──main│└──java│└──org.itstack.interview.jvm│├──classpath││├──impl│││├──CompositeEntry.java│││├──DirEntry.java│││├──WildcardEntry.java│││└──ZipEntry.java││├──Classpath.java││└──Entry.java│├──Cmd.java│└──Main.java└──test└──java└──org.itstack.interview.jvm.test└──HelloWorld.java 「以上」,工程结构就是按照 JVM 虚拟机规范,使用 Java 代码实现 JVM 中加载 class 文件部分内容。当然这部分还不包括解析,因为解析部分的代码非常庞大,我们先从把 .class 文件加载读取开始了解。 2. 代码讲解 2.1 定义类路径接口(Entry) publicinterfaceEntry{byte[]readClass(StringclassName)throwsIOException;staticEntrycreate(Stringpath){//File.pathSeparator;路径分隔符(win\linux)if(path.contains(File.pathSeparator)){returnnewCompositeEntry(path);}if(path.endsWith("*")){returnnewWildcardEntry(path);}if(path.endsWith(".jar")||path.endsWith(".JAR")||path.endsWith(".zip")||path.endsWith(".ZIP")){returnnewZipEntry(path);}returnnewDirEntry(path);}} 接口中提供了接口方法 readClass 和静态方法 create(String path)。 jdk1.8 是可以在接口中编写静态方法的,在设计上属于补全了抽象类的类似功能。这个静态方法主要是按照不同的路径地址类型,提供不同的解析方法。包括:CompositeEntry、WildcardEntry、ZipEntry、DirEntry,这四种。 接下来分别看每一种的具体实现 2.2 目录形式路径(DirEntry) publicclassDirEntryimplementsEntry{privatePathabsolutePath;publicDirEntry(Stringpath){//获取绝对路径this.absolutePath=Paths.get(path).toAbsolutePath();}@Overridepublicbyte[]readClass(StringclassName)throwsIOException{returnFiles.readAllBytes(absolutePath.resolve(className));}@OverridepublicStringtoString(){returnthis.absolutePath.toString();}} 目录形式的通过读取绝对路径下的文件,通过 Files.readAllBytes 方式获取字节码。 2.3 压缩包形式路径(ZipEntry) publicclassZipEntryimplementsEntry{privatePathabsolutePath;publicZipEntry(Stringpath){//获取绝对路径this.absolutePath=Paths.get(path).toAbsolutePath();}@Overridepublicbyte[]readClass(StringclassName)throwsIOException{try(FileSystemzipFs=FileSystems.newFileSystem(absolutePath,null)){returnFiles.readAllBytes(zipFs.getPath(className));}}@OverridepublicStringtoString(){returnthis.absolutePath.toString();}} 其实压缩包形式与目录形式,只有在文件读取上有包装差别而已。 FileSystems.newFileSystem 2.4 混合形式路径(CompositeEntry) publicclassCompositeEntryimplementsEntry{privatefinalList<Entry>entryList=newArrayList<>();publicCompositeEntry(StringpathList){String[]paths=pathList.split(File.pathSeparator);for(Stringpath:paths){entryList.add(Entry.create(path));}}@Overridepublicbyte[]readClass(StringclassName)throwsIOException{for(Entryentry:entryList){try{returnentry.readClass(className);}catch(Exceptionignored){//ignored}}thrownewIOException("classnotfound"+className);}@OverridepublicStringtoString(){String[]strs=newString[entryList.size()];for(inti=0;i<entryList.size();i++){strs[i]=entryList.get(i).toString();}returnString.join(File.pathSeparator,strs);}} File.pathSeparator,是一个分隔符属性,win/linux 有不同的类型,所以使用这个方法进行分割路径。 分割后的路径装到 List 集合中,这个过程属于拆分路径。 2.5 通配符类型路径(WildcardEntry) publicclassWildcardEntryextendsCompositeEntry{publicWildcardEntry(Stringpath){super(toPathList(path));}privatestaticStringtoPathList(StringwildcardPath){StringbaseDir=wildcardPath.replace("*","");//remove*try{returnFiles.walk(Paths.get(baseDir)).filter(Files::isRegularFile).map(Path::toString).filter(p->p.endsWith(".jar")||p.endsWith(".JAR")).collect(Collectors.joining(File.pathSeparator));}catch(IOExceptione){return"";}}} 这个类属于混合形式路径处理类的子类,唯一提供的方法就是把类路径解析出来。 2.6 类路径解析(Classpath) 启动类路径、扩展类路径、用户类路径,熟悉吗?是不经常看到这几句话,那么时候怎么实现的呢? 有了上面我们做的一些基础类的工作,接下来就是类解析的实际调用过程。代码如下: publicclassClasspath{privateEntrybootstrapClasspath;//启动类路径privateEntryextensionClasspath;//扩展类路径privateEntryuserClasspath;//用户类路径publicClasspath(StringjreOption,StringcpOption){//启动类&扩展类"C:\ProgramFiles\Java\jdk1.8.0_161\jre"bootstrapAndExtensionClasspath(jreOption);//用户类F:\..\org\itstack\demo\test\HelloWorldparseUserClasspath(cpOption);}privatevoidbootstrapAndExtensionClasspath(StringjreOption){StringjreDir=getJreDir(jreOption);//..jre/lib/*StringjreLibPath=Paths.get(jreDir,"lib")+File.separator+"*";bootstrapClasspath=newWildcardEntry(jreLibPath);//..jre/lib/ext/*StringjreExtPath=Paths.get(jreDir,"lib","ext")+File.separator+"*";extensionClasspath=newWildcardEntry(jreExtPath);}privatestaticStringgetJreDir(StringjreOption){if(jreOption!=null&&Files.exists(Paths.get(jreOption))){returnjreOption;}if(Files.exists(Paths.get("./jre"))){return"./jre";}Stringjh=System.getenv("JAVA_HOME");if(jh!=null){returnPaths.get(jh,"jre").toString();}thrownewRuntimeException("CannotfindJREfolder!");}privatevoidparseUserClasspath(StringcpOption){if(cpOption==null){cpOption=".";}userClasspath=Entry.create(cpOption);}publicbyte[]readClass(StringclassName)throwsException{className=className+".class";//[readClass]启动类路径try{returnbootstrapClasspath.readClass(className);}catch(Exceptionignored){//ignored}//[readClass]扩展类路径try{returnextensionClasspath.readClass(className);}catch(Exceptionignored){//ignored}//[readClass]用户类路径returnuserClasspath.readClass(className);}} 启动类路径,bootstrapClasspath.readClass(className); 扩展类路径,extensionClasspath.readClass(className); 用户类路径,userClasspath.readClass(className); 这回就看到它们具体在哪使用了吧!有了具体的代码也就方便理解了 2.7 加载类测试验证 privatestaticvoidstartJVM(Cmdcmd){Classpathcp=newClasspath(cmd.jre,cmd.classpath);System.out.printf("classpath:%s class:%s args:%s\n",cp,cmd.getMainClass(),cmd.getAppArgs());//获取classNameStringclassName=cmd.getMainClass().replace(".","/");try{byte[]classData=cp.readClass(className);System.out.println(Arrays.toString(classData));}catch(Exceptione){System.out.println("Couldnotfindorloadmainclass"+cmd.getMainClass());e.printStackTrace();}} 这段就是使用 Classpath 类进行类路径加载,这里我们测试加载 java.lang.String 类。你可以加载其他的类,或者自己写的类 配置IDEA,program arguments 参数: -Xjre "C:\Program Files\Java\jdk1.8.0_161\jre" java.lang.String 另外这里读取出的 class 文件信息,打印的是 byte 类型信息。 「测试结果」 [-54,-2,-70,-66,0,0,0,52,2,28,3,0,0,-40,0,3,0,0,-37,-1,3,0,0,-33,-1,3,0,1,0,0,8,0,15,8,0,61,8,0,85,8,0,88,8,0,89,8,0,112,8,0,-81,8,0,-75,8,0,-47,8,0,-45,1,0,0,1,0,3,40,41,73,1,0,20,40,41,76,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,59,1,0,20,40,41,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,1,0,3,40,41,86,1,0,3,40,41,90,1,0,4,40,41,91,...] 这块部分截取的程序运行打印结果,就是读取的 class 文件信息,只不过暂时还不能看出什么。接下来我们再把它翻译过来! 五、解析字节码文件 JVM 在把 class 文件加载完成后,接下来就进入链接的过程,这个过程包括了内容的校验、准备和解析,其实就是把 byte 类型 class 翻译过来,做相应的操作。 整个这个过程内容相对较多,这里只做部分逻辑的实现和讲解。如果读者感兴趣可以阅读小傅哥的《用Java实现JVM》专栏。 1. 提取部分字节码 //取部分字节码:java.lang.Stringprivatestaticbyte[]classData={-54,-2,-70,-66,0,0,0,52,2,26,3,0,0,-40,0,3,0,0,-37,-1,3,0,0,-33,-1,3,0,1,0,0,8,0,59,8,0,83,8,0,86,8,0,87,8,0,110,8,0,-83,8,0,-77,8,0,-49,8,0,-47,1,0,3,40,41,73,1,0,20,40,41,76,106,97,118,97,47,108,97,110,103,47,79,98,106,101,99,116,59,1,0,20,40,41,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,1,0,3,40,41,86,1,0,3,40,41,90,1,0,4,40,41,91,66,1,0,4,40,41,91,67,1,0,4,40,67,41,67,1,0,21,40,68,41,76,106,97,118,97,47,108,97,110,103,47,83,116,114,105,110,103,59,1,0,4,40,73,41,67,1,0,4}; java.lang.String 解析出来的字节码内容较多,当然包括的内容也多,比如魔数、版本、类、常量、方法等等。所以我们这里只截取部分进行进行解析。 2. 解析魔数并校验 很多文件格式都会规定满足该格式的文件必须以某几个固定字节开头,这几个字节主要起到标识作用,叫作魔数(magic number)。 例如; PDF文件以4字节“%PDF”(0x25、0x50、0x44、0x46)开头, ZIP文件以2字节“PK”(0x50、0x4B)开头 class文件以4字节“0xCAFEBABE”开头 privatestaticvoidreadAndCheckMagic(){System.out.println("\r\n------------校验魔数------------");//从class字节码中读取前四位byte[]magic_byte=newbyte[4];System.arraycopy(classData,0,magic_byte,0,4);//将4位byte字节转成16进制字符串Stringmagic_hex_str=newBigInteger(1,magic_byte).toString(16);System.out.println("magic_hex_str:"+magic_hex_str);//byte_magic_str是16进制的字符串,cafebabe,因为java中没有无符号整型,所以如果想要无符号只能放到更高位中longmagic_unsigned_int32=Long.parseLong(magic_hex_str,16);System.out.println("magic_unsigned_int32:"+magic_unsigned_int32);//魔数比对,一种通过字符串比对,另外一种使用假设的无符号16进制比较。如果使用无符号比较需要将0xCAFEBABE &0x0FFFFFFFFL与运算System.out.println("0xCAFEBABE &0x0FFFFFFFFL:"+(0xCAFEBABE&0x0FFFFFFFFL));if(magic_unsigned_int32==(0xCAFEBABE&0x0FFFFFFFFL)){System.out.println("class字节码魔数无符号16进制数值一致校验通过");}else{System.out.println("class字节码魔数无符号16进制数值一致校验拒绝");}} 读取字节码中的前四位, -54, -2, -70, -66,将这四位转换为16进制。 因为 java 中是没有无符号整型的,所以只能用更高位存放。 解析后就是魔数的对比,看是否与 CAFEBABE 一致。 「测试结果」 ------------校验魔数------------magic_hex_str:cafebabemagic_unsigned_int32:34056915820xCAFEBABE&0x0FFFFFFFFL:3405691582class字节码魔数无符号16进制数值一致校验通过 3. 解析版本号信息 刚才我们已经读取了4位魔数信息,接下来再读取2位,是版本信息。 魔数之后是class文件的次版本号和主版本号,都是u2类型。假设某class文件的主版本号是M,次版本号是m,那么完整的版本号可以表示成“M.m”的形式。次版本号只在J2SE 1.2之前用过,从1.2开始基本上就没有什么用了(都是0)。主版本号在J2SE 1.2之前是45,从1.2开始,每次有大版本的Java版本发布,都会加1{45、46、47、48、49、50、51、52} privatestaticvoidreadAndCheckVersion(){System.out.println("\r\n------------校验版本号------------");//从class字节码第4位开始读取,读取2位byte[]minor_byte=newbyte[2];System.arraycopy(classData,4,minor_byte,0,2);//将2位byte字节转成16进制字符串Stringminor_hex_str=newBigInteger(1,minor_byte).toString(16);System.out.println("minor_hex_str:"+minor_hex_str);//minor_unsigned_int32转成无符号16进制intminor_unsigned_int32=Integer.parseInt(minor_hex_str,16);System.out.println("minor_unsigned_int32:"+minor_unsigned_int32);//从class字节码第6位开始读取,读取2位byte[]major_byte=newbyte[2];System.arraycopy(classData,6,major_byte,0,2);//将2位byte字节转成16进制字符串Stringmajor_hex_str=newBigInteger(1,major_byte).toString(16);System.out.println("major_hex_str:"+major_hex_str);//major_unsigned_int32转成无符号16进制intmajor_unsigned_int32=Integer.parseInt(major_hex_str,16);System.out.println("major_unsigned_int32:"+major_unsigned_int32);System.out.println("版本号:"+major_unsigned_int32+"."+minor_unsigned_int32);} 这里有一个小技巧,class 文件解析出来是一整片的内容,JVM 需要按照虚拟机规范,一段一段的解析出所有的信息。 同样这里我们需要把2位byte转换为16进制信息,并继续从第6位继续读取2位信息。组合出来的才是版本信息。 「测试结果」 ------------校验版本号------------minor_hex_str:0minor_unsigned_int32:0major_hex_str:34major_unsigned_int32:52版本号:52.0 4. 解析全部内容对照 按照 JVM 的加载过程,其实远不止魔数和版本号信息,还有很多其他内容,这里我们可以把测试结果展示出来,方便大家有一个学习结果的比对印象。 classpath:org.itstack.demo.jvm.classpath.Classpath@4bf558aaclass:java.lang.Stringargs:nullversion:52.0constantscount:540accessflags:0x31thisclass:java/lang/Stringsuperclass:java/lang/Objectinterfaces:[java/io/Serializable,java/lang/Comparable,java/lang/CharSequence]fieldscount:5value[ChashIserialVersionUIDJserialPersistentFields[Ljava/io/ObjectStreamField;CASE_INSENSITIVE_ORDERLjava/util/Comparator;methodscount:94<init>()V<init>(Ljava/lang/String;)V<init>([C)V<init>([CII)V<init>([III)V<init>([BIII)V<init>([BI)VcheckBounds([BII)V<init>([BIILjava/lang/String;)V<init>([BIILjava/nio/charset/Charset;)V<init>([BLjava/lang/String;)V<init>([BLjava/nio/charset/Charset;)V<init>([BII)V<init>([B)V<init>(Ljava/lang/StringBuffer;)V<init>(Ljava/lang/StringBuilder;)V<init>([CZ)Vlength()IisEmpty()ZcharAt(I)CcodePointAt(I)IcodePointBefore(I)IcodePointCount(II)IoffsetByCodePoints(II)IgetChars([CI)VgetChars(II[CI)VgetBytes(II[BI)VgetBytes(Ljava/lang/String;)[BgetBytes(Ljava/nio/charset/Charset;)[BgetBytes()[Bequals(Ljava/lang/Object;)ZcontentEquals(Ljava/lang/StringBuffer;)ZnonSyncContentEquals(Ljava/lang/AbstractStringBuilder;)ZcontentEquals(Ljava/lang/CharSequence;)ZequalsIgnoreCase(Ljava/lang/String;)ZcompareTo(Ljava/lang/String;)IcompareToIgnoreCase(Ljava/lang/String;)IregionMatches(ILjava/lang/String;II)ZregionMatches(ZILjava/lang/String;II)ZstartsWith(Ljava/lang/String;I)ZstartsWith(Ljava/lang/String;)ZendsWith(Ljava/lang/String;)ZhashCode()IindexOf(I)IindexOf(II)IindexOfSupplementary(II)IlastIndexOf(I)IlastIndexOf(II)IlastIndexOfSupplementary(II)IindexOf(Ljava/lang/String;)IindexOf(Ljava/lang/String;I)IindexOf([CIILjava/lang/String;I)IindexOf([CII[CIII)IlastIndexOf(Ljava/lang/String;)IlastIndexOf(Ljava/lang/String;I)IlastIndexOf([CIILjava/lang/String;I)IlastIndexOf([CII[CIII)Isubstring(I)Ljava/lang/String;substring(II)Ljava/lang/String;subSequence(II)Ljava/lang/CharSequence;concat(Ljava/lang/String;)Ljava/lang/String;replace(CC)Ljava/lang/String;matches(Ljava/lang/String;)Zcontains(Ljava/lang/CharSequence;)ZreplaceFirst(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;replaceAll(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;split(Ljava/lang/String;I)[Ljava/lang/String;split(Ljava/lang/String;)[Ljava/lang/String;join(Ljava/lang/CharSequence;[Ljava/lang/CharSequence;)Ljava/lang/String;join(Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;toLowerCase(Ljava/util/Locale;)Ljava/lang/String;toLowerCase()Ljava/lang/String;toUpperCase(Ljava/util/Locale;)Ljava/lang/String;toUpperCase()Ljava/lang/String;trim()Ljava/lang/String;toString()Ljava/lang/String;toCharArray()[Cformat(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;format(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;valueOf(Ljava/lang/Object;)Ljava/lang/String;valueOf([C)Ljava/lang/String;valueOf([CII)Ljava/lang/String;copyValueOf([CII)Ljava/lang/String;copyValueOf([C)Ljava/lang/String;valueOf(Z)Ljava/lang/String;valueOf(C)Ljava/lang/String;valueOf(I)Ljava/lang/String;valueOf(J)Ljava/lang/String;valueOf(F)Ljava/lang/String;valueOf(D)Ljava/lang/String;intern()Ljava/lang/String;compareTo(Ljava/lang/Object;)I<clinit>()VProcessfinishedwithexitcode0 如果大家对这部分验证、准备、解析,的实现过程感兴趣,可以参照这部分用Java实现的JVM源码:https://github.com/fuzhengwei/itstack-demo-jvm 六、总结 学习 JVM 最大的问题是不好实践,所以本文以案例实操的方式,学习 JVM 的加载解析过程。也让更多的对 JVM 感兴趣的研发,能更好的接触到 JVM 并深入的学习。 有了以上这段代码,大家可以参照 JVM 虚拟机规范,在调试Java版本的JVM,这样就可以非常容易理解整个JVM的加载过程,都做了什么。 如果大家需要文章中一些原图 xmind 或者源码,可以添加作者小傅哥(fustack),或者关注公众号:bugstack虫洞栈进行获取。好了,本章节就扯到这,后续还有很多努力,持续原创,感谢大家的支持! 七、系列推荐 ReentrantLock之公平锁讲解和实现 除了JDK、CGLIB,还有3种类代理方式?面试又卡住! 面试官,ThreadLocal 你要这么问,我就挂了! 手写线程池,对照学习ThreadPoolExecutor线程池实现原理! 一次代码评审,差点过不了试用期! bugstack虫洞栈 沉淀、分享、成长,让自己和他人都能有所收获! 作者小傅哥多年从事一线互联网Java开发,从19年开始编写工作和学习历程的技术汇总,旨在为大家提供一个较清晰详细的核心技能学习文档。如果本文能为您提供帮助,请给予支持(关注、点赞、分享)! 感谢支持小傅哥原创,欢迎点击在看和转发 本文分享自微信公众号 - bugstack虫洞栈(bugstack)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

牛逼,手撸一个SpringBoot缓存系统!

缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。 本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统。 一、通用缓存接口 1、缓存基础算法 FIFO(First In First Out),先进先出,和OS里的FIFO思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。 LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。 LRU(Least Recently Used),最近最少使用,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据移除。 2、接口定义 简单定义缓存接口,大致可以抽象如下: packagecom.power.demo.cache.contract;importjava.util.function.Function;/***缓存提供者接口**/publicinterfaceCacheProviderService{/***查询缓存**@paramkey缓存键不可为空**/<TextendsObject>Tget(Stringkey);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/<TextendsObject>Tget(Stringkey,Function<String,T>function);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime);/***设置缓存键值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/<TextendsObject>voidset(Stringkey,Tobj);/***设置缓存键值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@param expireTime 过期时间(单位:毫秒)可为空**/<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime);/***移除缓存**@paramkey缓存键不可为空**/voidremove(Stringkey);/***是否存在缓存**@paramkey缓存键不可为空**/booleancontains(Stringkey);} 注意,这里列出的只是常见缓存功能接口,一些在特殊场景下用到的统计类的接口、分布式锁、自增(减)等功能不在讨论范围之内。 Get相关方法,注意多个参数的情况,缓存接口里面传人的Function,这是Java8提供的函数式接口,虽然支持的入参个数有限(这里你会非常怀念.NET下的Func委托),但是仅对Java这个语言来说,这真是一个重大的进步^_^。 接口定义好了,下面就要实现缓存提供者程序了。按照存储类型的不同,本文简单实现最常用的两种缓存提供者:本地缓存和分布式缓存。 二、本地缓存 本地缓存,也就是JVM级别的缓存(本地缓存可以认为是直接在进程内通信调用,而分布式缓存则需要通过网络进行跨进程通信调用),一般有很多种实现方式,比如直接使用Hashtable、ConcurrentHashMap等天生线程安全的集合作为缓存容器,或者使用一些成熟的开源组件,如EhCache、Guava Cache等。本文选择上手简单的Guava缓存。 1、什么是Guava Guava,简单来说就是一个开发类库,且是一个非常丰富强大的开发工具包,号称可以让使用Java语言更令人愉悦,主要包括基本工具类库和接口、缓存、发布订阅风格的事件总线等。在实际开发中,我用的最多的是集合、缓存和常用类型帮助类,很多人都对这个类库称赞有加。 2、添加依赖 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency> 3、实现接口 packagecom.power.demo.cache.impl;importcom.google.common.cache.Cache;importcom.google.common.cache.CacheBuilder;importcom.google.common.collect.Maps;importcom.power.demo.cache.contract.CacheProviderService;importcom.power.demo.common.AppConst;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importjava.util.Map;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;importjava.util.function.Function;/**本地缓存提供者服务(GuavaCache)**/@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)@Qualifier("localCacheService")publicclassLocalCacheProviderImplimplementsCacheProviderService{privatestaticMap<String,Cache<String,Object>>_cacheMap=Maps.newConcurrentMap();static{Cache<String,Object>cacheContainer=CacheBuilder.newBuilder().maximumSize(AppConst.CACHE_MAXIMUM_SIZE).expireAfterWrite(AppConst.CACHE_MINUTE,TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次访问后的一段时间移出.recordStats()//开启统计功能.build();_cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE),cacheContainer);}/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=get(key,null,null,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=get(key,function,key,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=get(key,function,funcParm,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime){Tobj=get(key,function,key,expireTime);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime){Tobj=null;if(StringUtils.isEmpty(key)==true){returnobj;}expireTime=getExpireTime(expireTime);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);try{if(function==null){obj=(T)cacheContainer.getIfPresent(key);}else{finalLongcachedTime=expireTime;obj=(T)cacheContainer.get(key,()->{TretObj=function.apply(funcParm);returnretObj;});}}catch(Exceptione){e.printStackTrace();}returnobj;}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){set(key,obj,AppConst.CACHE_MINUTE);}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@param expireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){if(StringUtils.isEmpty(key)==true){return;}if(obj==null){return;}expireTime=getExpireTime(expireTime);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);cacheContainer.put(key,obj);}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){if(StringUtils.isEmpty(key)==true){return;}longexpireTime=getExpireTime(AppConst.CACHE_MINUTE);Cache<String,Object>cacheContainer=getCacheContainer(expireTime);cacheContainer.invalidate(key);}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}privatestaticLocklock=newReentrantLock();privateCache<String,Object>getCacheContainer(LongexpireTime){Cache<String,Object>cacheContainer=null;if(expireTime==null){returncacheContainer;}StringmapKey=String.valueOf(expireTime);if(_cacheMap.containsKey(mapKey)==true){cacheContainer=_cacheMap.get(mapKey);returncacheContainer;}try{lock.lock();cacheContainer=CacheBuilder.newBuilder().maximumSize(AppConst.CACHE_MAXIMUM_SIZE).expireAfterWrite(expireTime,TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次访问后的一段时间移出.recordStats()//开启统计功能.build();_cacheMap.put(mapKey,cacheContainer);}finally{lock.unlock();}returncacheContainer;}/***获取过期时间单位:毫秒**@paramexpireTime传人的过期时间单位毫秒如小于1分钟,默认为10分钟**/privateLonggetExpireTime(LongexpireTime){Longresult=expireTime;if(expireTime==null||expireTime<AppConst.CACHE_MINUTE/10){result=AppConst.CACHE_MINUTE;}returnresult;}} 4、注意事项 Guava Cache初始化容器时,支持缓存过期策略,类似FIFO、LRU和LFU等算法。 expireAfterWrite:最后一次写入后的一段时间移出。 expireAfterAccess:最后一次访问后的一段时间移出。 Guava Cache对缓存过期时间的设置实在不够友好。常见的应用场景,比如,有些几乎不变的基础数据缓存1天,有些热点数据缓存2小时,有些会话数据缓存5分钟等等。 通常我们认为设置缓存的时候带上缓存的过期时间是非常容易的,而且只要一个缓存容器实例即可,比如.NET下的ObjectCache、System.Runtime.Cache等等。 但是Guava Cache不是这个实现思路,如果缓存的过期时间不同,Guava的CacheBuilder要初始化多份Cache实例。 好在我在实现的时候注意到了这个问题,并且提供了解决方案,可以看到getCacheContainer这个函数,根据过期时长做缓存实例判断,就算不同过期时间的多实例缓存也是完全没有问题的。 三、分布式缓存 分布式缓存产品非常多,本文使用应用普遍的Redis,在Spring Boot应用中使用Redis非常简单。 1、什么是Redis Redis是一款开源(BSD许可)的、用C语言写成的高性能的键-值存储(key-value store)。它常被称作是一款数据结构服务器(data structure server)。它可以被用作缓存、消息中间件和数据库,在很多应用中,经常看到有人选择使用Redis做缓存,实现分布式锁和分布式Session等。作为缓存系统时,和经典的KV结构的Memcached非常相似,但又有很多不同。 Redis支持丰富的数据类型。Redis的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等数据类型。对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。 Redis的数据类型: Keys:非二进制安全的字符类型( not binary-safe strings ),由于key不是binary safe的字符串,所以像“my key”和“mykey\n”这样包含空格和换行的key是不允许的。Values:Strings、Hash、Lists、 Sets、 Sorted sets。考虑到Redis单线程操作模式,Value的粒度不应该过大,缓存的值越大,越容易造成阻塞和排队。 为了获得优异的性能,Redis采用了内存中(in-memory)数据集(dataset)的方式。同时,Redis支持数据的持久化,你可以每隔一段时间将数据集转存到磁盘上(snapshot),或者在日志尾部追加每一条操作命令(append only file,aof)。 Redis同样支持主从复制(master-slave replication),并且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、网络断开自动重连等功能。 同时Redis还具有其它一些特性,其中包括简单的事物支持、发布订阅 ( pub/sub)、管道(pipeline)和虚拟内存(vm)等 。 2、添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency> 3、配置Redis 在application.properties配置文件中,配置Redis常用参数: ##Redis缓存相关配置#Redis数据库索引(默认为0)spring.redis.database=0#Redis服务器地址spring.redis.host=127.0.0.1#Redis服务器端口spring.redis.port=6379#Redis服务器密码(默认为空)spring.redis.password=123321#Redis连接超时时间默认:5分钟(单位:毫秒)spring.redis.timeout=300000ms#Redis连接池最大连接数(使用负值表示没有限制)spring.redis.jedis.pool.max-active=512#Redis连接池中的最小空闲连接spring.redis.jedis.pool.min-idle=0#Redis连接池中的最大空闲连接spring.redis.jedis.pool.max-idle=8#Redis连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.jedis.pool.max-wait=-1ms 常见的需要注意的是最大连接数(spring.redis.jedis.pool.max-active )和超时时间(spring.redis.jedis.pool.max-wait)。Redis在生产环境中出现故障的频率经常和这两个参数息息相关。 接着定义一个继承自CachingConfigurerSupport(请注意cacheManager和keyGenerator这两个方法在子类的实现)的RedisConfig类: packagecom.power.demo.cache.config;importorg.springframework.cache.CacheManager;importorg.springframework.cache.annotation.CachingConfigurerSupport;importorg.springframework.cache.annotation.EnableCaching;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.cache.RedisCacheManager;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.StringRedisSerializer;/***Redis缓存配置类*/@Configuration@EnableCachingpublicclassRedisConfigextendsCachingConfigurerSupport{@BeanpublicCacheManagercacheManager(RedisConnectionFactoryconnectionFactory){returnRedisCacheManager.create(connectionFactory);}@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryfactory){RedisTemplate<String,Object>template=newRedisTemplate<>();//Jedis的Key和Value的序列化器默认值是JdkSerializationRedisSerializer//经实验,JdkSerializationRedisSerializer通过RedisDesktopManager看到的键值对不能正常解析//设置key的序列化器template.setKeySerializer(newStringRedisSerializer());////设置value的序列化器默认值是JdkSerializationRedisSerializer//使用Jackson序列化器的问题是,复杂对象可能序列化失败,比如JodaTime的DateTime类型////使用Jackson2,将对象序列化为JSON//Jackson2JsonRedisSerializerjackson2JsonRedisSerializer=newJackson2JsonRedisSerializer(Object.class);////json转对象类,不设置默认的会将json转成hashmap//ObjectMapperom=newObjectMapper();//om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);//jackson2JsonRedisSerializer.setObjectMapper(om);//template.setValueSerializer(jackson2JsonRedisSerializer);//将redis连接工厂设置到模板类中template.setConnectionFactory(factory);returntemplate;}////自定义缓存key生成策略//@Bean//publicKeyGeneratorkeyGenerator(){//returnnewKeyGenerator(){//@Override//publicObjectgenerate(Objecttarget,java.lang.reflect.Methodmethod,Object...params){//StringBuffersb=newStringBuffer();//sb.append(target.getClass().getName());//sb.append(method.getName());//for(Objectobj:params){//if(obj==null){//continue;//}//sb.append(obj.toString());//}//returnsb.toString();//}//};//}} 在RedisConfig这个类上加上@EnableCaching这个注解,这个注解会被Spring发现,并且会创建一个切面(aspect) 并触发Spring缓存注解的切点(pointcut)。据所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值。 cacheManager方法,申明一个缓存管理器(CacheManager)的bean,作用就是@EnableCaching这个切面在新增缓存或者删除缓存的时候会调用这个缓存管理器的方法。keyGenerator方法,可以根据需求自定义缓存key生成策略。 而redisTemplate方法,则主要是设置Redis模板类,比如键和值的序列化器(从这里可以看出,Redis的键值对必须可序列化)、redis连接工厂等。 RedisTemplate支持的序列化器主要有如下几种: JdkSerializationRedisSerializer:使用Java序列化; StringRedisSerializer:序列化String类型的key和value; GenericToStringSerializer:使用Spring转换服务进行序列化; JacksonJsonRedisSerializer:使用Jackson 1,将对象序列化为JSON; Jackson2JsonRedisSerializer:使用Jackson 2,将对象序列化为JSON; OxmSerializer:使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实现序列化,用于XML序列化; 注意:RedisTemplate的键和值序列化器,默认情况下都是JdkSerializationRedisSerializer,它们都可以自定义设置序列化器。 推荐将字符串键使用StringRedisSerializer序列化器,因为运维的时候好排查问题,JDK序列化器的也能识别,但是可读性稍差(是因为缓存服务器没有JRE吗?),见如下效果: 而值序列化器则要复杂的多,很多人推荐使用Jackson2JsonRedisSerializer序列化器,但是实际开发过程中,经常有人碰到反序列化错误,经过排查多数都和Jackson2JsonRedisSerializer这个序列化器有关。 4、实现接口 使用RedisTemplate,在Spring Boot中调用Redis接口比直接调用Jedis简单多了。 packagecom.power.demo.cache.impl;importcom.power.demo.cache.contract.CacheProviderService;importcom.power.demo.common.AppConst;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.ValueOperations;importorg.springframework.util.StringUtils;importjavax.annotation.Resource;importjava.io.Serializable;importjava.util.concurrent.TimeUnit;importjava.util.function.Function;@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)@Qualifier("redisCacheService")publicclassRedisCacheProviderImplimplementsCacheProviderService{@ResourceprivateRedisTemplate<Serializable,Object>redisTemplate;/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=get(key,null,null,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=get(key,function,key,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=get(key,function,funcParm,AppConst.CACHE_MINUTE);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime){Tobj=get(key,function,key,expireTime);returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime){Tobj=null;if(StringUtils.isEmpty(key)==true){returnobj;}expireTime=getExpireTime(expireTime);try{ValueOperations<Serializable,Object>operations=redisTemplate.opsForValue();obj=(T)operations.get(key);if(function!=null&&obj==null){obj=function.apply(funcParm);if(obj!=null){set(key,obj,expireTime);//设置缓存信息}}}catch(Exceptione){e.printStackTrace();}returnobj;}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){set(key,obj,AppConst.CACHE_MINUTE);}/***设置缓存键值直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){if(StringUtils.isEmpty(key)==true){return;}if(obj==null){return;}expireTime=getExpireTime(expireTime);ValueOperations<Serializable,Object>operations=redisTemplate.opsForValue();operations.set(key,obj);redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){if(StringUtils.isEmpty(key)==true){return;}redisTemplate.delete(key);}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}/***获取过期时间单位:毫秒**@paramexpireTime传人的过期时间单位毫秒如小于1分钟,默认为10分钟**/privateLonggetExpireTime(LongexpireTime){Longresult=expireTime;if(expireTime==null||expireTime<AppConst.CACHE_MINUTE/10){result=AppConst.CACHE_MINUTE;}returnresult;}} 注意:很多教程里都讲到通过注解的方式(@Cacheable,@CachePut、@CacheEvict和@Caching)实现数据缓存,根据实践,我个人是不推崇这种使用方式的。 四、缓存“及时”过期问题 这个也是开发和运维过程中非常经典的问题。 有些公司写缓存客户端的时候,会给每个团队分别定义一个Area,但是这个只能做到缓存键的分布区分,不能保证缓存“实时”有效的过期。 多年以前我写过一篇结合实际情况的文章,也就是加上缓存版本,请猛击这里 ,算是提供了一种相对有效的方案,不过高并发站点要慎重,防止发生雪崩效应。 Redis还有一些其他常见问题,比如:Redis的字符串类型Key和Value都有限制,且都是不能超过512M,请猛击这里。还有最大连接数和超时时间设置等问题,本文就不再一一列举了。 五、二级缓存 在配置文件中,加上缓存提供者开关: ##是否启用本地缓存spring.power.isuselocalcache=1##是否启用Redis缓存spring.power.isuserediscache=1 缓存提供者程序都实现好了,我们会再包装一个调用外观类PowerCacheBuilder,加上缓存版本控制,可以轻松自如地控制和切换缓存,code talks: packagecom.power.demo.cache;importcom.google.common.collect.Lists;importcom.power.demo.cache.contract.CacheProviderService;importcom.power.demo.common.AppConst;importcom.power.demo.common.AppField;importcom.power.demo.util.ConfigUtil;importcom.power.demo.util.PowerLogger;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importjava.util.List;importjava.util.UUID;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;importjava.util.function.Function;/**支持多缓存提供程序多级缓存的缓存帮助类**/@Configuration@ComponentScan(basePackages=AppConst.BASE_PACKAGE_NAME)publicclassPowerCacheBuilder{@Autowired@Qualifier("localCacheService")privateCacheProviderServicelocalCacheService;@Autowired@Qualifier("redisCacheService")privateCacheProviderServiceredisCacheService;privatestaticList<CacheProviderService>_listCacheProvider=Lists.newArrayList();privatestaticfinalLockproviderLock=newReentrantLock();/***初始化缓存提供者默认优先级:先本地缓存,后分布式缓存**/privateList<CacheProviderService>getCacheProviders(){if(_listCacheProvider.size()>0){return_listCacheProvider;}//线程安全try{providerLock.tryLock(1000,TimeUnit.MILLISECONDS);if(_listCacheProvider.size()>0){return_listCacheProvider;}StringisUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);CacheProviderServicecacheProviderService=null;//启用本地缓存if("1".equalsIgnoreCase(isUseCache)){_listCacheProvider.add(localCacheService);}isUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);//启用Redis缓存if("1".equalsIgnoreCase(isUseCache)){_listCacheProvider.add(redisCacheService);resetCacheVersion();//设置分布式缓存版本号}PowerLogger.info("初始化缓存提供者成功,共有"+_listCacheProvider.size()+"个");}catch(Exceptione){e.printStackTrace();_listCacheProvider=Lists.newArrayList();PowerLogger.error("初始化缓存提供者发生异常:{}",e);}finally{providerLock.unlock();}return_listCacheProvider;}/***查询缓存**@paramkey缓存键不可为空**/public<TextendsObject>Tget(Stringkey){Tobj=null;//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){obj=provider.get(key);if(obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,funcParm);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,funcParm);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>Tget(Stringkey,Function<String,T>function,longexpireTime){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,expireTime);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,expireTime);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,longexpireTime){Tobj=null;for(CacheProviderServiceprovider:getCacheProviders()){if(obj==null){obj=provider.get(key,function,funcParm,expireTime);}elseif(function!=null&&obj!=null){//查询并设置其他缓存提供者程序缓存provider.get(key,function,funcParm,expireTime);}//如果callable函数为空而缓存对象不为空及时跳出循环并返回if(function==null&&obj!=null){returnobj;}}returnobj;}/***设置缓存键值直接向缓存中插入或覆盖值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/public<TextendsObject>voidset(Stringkey,Tobj){//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){provider.set(key,obj);}}/***设置缓存键值直接向缓存中插入或覆盖值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@paramexpireTime 过期时间(单位:毫秒)可为空**/public<TextendsObject>voidset(Stringkey,Tobj,LongexpireTime){//key=generateVerKey(key);//构造带版本的缓存键for(CacheProviderServiceprovider:getCacheProviders()){provider.set(key,obj,expireTime);}}/***移除缓存**@paramkey缓存键不可为空**/publicvoidremove(Stringkey){//key=generateVerKey(key);//构造带版本的缓存键if(StringUtils.isEmpty(key)==true){return;}for(CacheProviderServiceprovider:getCacheProviders()){provider.remove(key);}}/***是否存在缓存**@paramkey缓存键不可为空**/publicbooleancontains(Stringkey){booleanexists=false;//key=generateVerKey(key);//构造带版本的缓存键if(StringUtils.isEmpty(key)==true){returnexists;}Objectobj=get(key);if(obj!=null){exists=true;}returnexists;}/***获取分布式缓存版本号**/publicStringgetCacheVersion(){Stringversion="";booleanisUseCache=checkUseRedisCache();//未启用Redis缓存if(isUseCache==false){returnversion;}version=redisCacheService.get(AppConst.CACHE_VERSION_KEY);returnversion;}/***重置分布式缓存版本如果启用分布式缓存,设置缓存版本**/publicStringresetCacheVersion(){Stringversion="";booleanisUseCache=checkUseRedisCache();//未启用Redis缓存if(isUseCache==false){returnversion;}//设置缓存版本version=String.valueOf(Math.abs(UUID.randomUUID().hashCode()));redisCacheService.set(AppConst.CACHE_VERSION_KEY,version);returnversion;}/***如果启用分布式缓存,获取缓存版本,重置查询的缓存key,可以实现相对实时的缓存过期控制*<p>*如没有启用分布式缓存,缓存key不做修改,直接返回**/publicStringgenerateVerKey(Stringkey){Stringresult=key;if(StringUtils.isEmpty(key)==true){returnresult;}booleanisUseCache=checkUseRedisCache();//没有启用分布式缓存,缓存key不做修改,直接返回if(isUseCache==false){returnresult;}Stringversion=redisCacheService.get(AppConst.CACHE_VERSION_KEY);if(StringUtils.isEmpty(version)==true){returnresult;}result=String.format("%s_%s",result,version);returnresult;}/***验证是否启用分布式缓存**/privatebooleancheckUseRedisCache(){booleanisUseCache=false;StringstrIsUseCache=ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);isUseCache="1".equalsIgnoreCase(strIsUseCache);returnisUseCache;}} 单元测试如下: @TestpublicvoidtestCacheVerson()throwsException{Stringversion=cacheBuilder.getCacheVersion();System.out.println(String.format("当前缓存版本:%s",version));StringcacheKey=cacheBuilder.generateVerKey("goods778899");GoodsVOgoodsVO=newGoodsVO();goodsVO.setGoodsId(UUID.randomUUID().toString());goodsVO.setCreateTime(newDate());goodsVO.setCreateDate(newDateTime(newDate()));goodsVO.setGoodsType(1024);goodsVO.setGoodsCode("123456789");goodsVO.setGoodsName("我的测试商品");cacheBuilder.set(cacheKey,goodsVO);GoodsVOgoodsVO1=cacheBuilder.get(cacheKey);Assert.assertNotNull(goodsVO1);version=cacheBuilder.resetCacheVersion();System.out.println(String.format("重置后的缓存版本:%s",version));cacheKey=cacheBuilder.generateVerKey("goods112233");cacheBuilder.set(cacheKey,goodsVO);GoodsVOgoodsVO2=cacheBuilder.get(cacheKey);Assert.assertNotNull(goodsVO2);Assert.assertTrue("两个缓存对象的主键相同",goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId()));} 一个满足基本功能的多级缓存系统就好了。 在Spring Boot应用中使用缓存则非常简洁,选择调用上面包装好的缓存接口即可。 StringcacheKey=_cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid."+request.getGoodsId());GoodsVOgoodsVO=_cacheBuilder.get(cacheKey,_goodsService::getGoodsByGoodsId,request.getGoodsId()); 到这里Spring Boot业务系统开发中最常用到的ORM,缓存和队列三板斧就介绍完了。 在开发的过程中你会发现,Java真的是非常非常中规中矩的语言,你需要不断折腾并熟悉常见的开源中间件和工具,开源的轮子实在是太丰富,多尝试几个,实践出真知。 来源:cnblogs.com/jeffwongishandsome 本文分享自微信公众号 - IT老哥(dys_family)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册