首页 文章 精选 留言 我的

精选列表

搜索[水印魔术手],共7323篇文章
优秀的个人博客,低调大师

为了搞清楚类加载,竟然手撸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源创计划”,欢迎正在阅读的你也加入,一起分享。

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

吃鸡手游竟然是Python写的?

“吃鸡”有多热,已经不用多说。但你知道吗?你热爱的“吃鸡”可能是用Python写的。 网易的两大游戏客户端引擎,NeoX 和 Messiah,都是使用 Python 作为脚本语言的。具体来说,服务器端可以说是完全Python,毕竟能够热更新的Python可以不停服!当然游戏的客户端核心引擎部分,还是用C++,一些类似基本渲染还是用C++来实现的。 其实用Python来写大型游戏也不是什么新闻了,新手教程都能出电子书的星际生存游戏——《EVE》也是用python写的。说得更具体点,是Stackless Python。Stackless是在Python基础上编写的一套微线程框架,它能在不产生大量Python自身额外开销的情况下同时容纳数百万条的线程。 软妹子也要学Pyth on 听起来这么高大上的Python是不是很难学?并没有。 北邮在2010年就出版过一本《和孩子一起学Python》,Python真的是容易入门到可以作为亲子娱乐项目的编程语言。 我第一次见到这本书是在一个学中文的妹子那,当时内心活动非常复杂:什么?妹子你都有孩子了? 不过了解后知道,妹子为了能够扒下豆瓣top250电影的列表,所以打算自学Python,然后妹子又向我推荐了MOOC上嵩天老师的Python课。 妹子讲嵩天老师的时候眼里有光,她说嵩天老师声音特别好听,而且把Python爬虫课讲得让她一个文科生都听懂了。 他可能是Coding届的邓布利多 嵩天是北京理工大学计算机学院副教授,清华工学博士,同时也是Python圈的“安利大大”。 他在MOOC的Python基础课是超级网红课,是很多编程小白从0到1的转折点。 嵩天老师其实是非典型性工科男,他用事实告诉你编程也可以很文艺。比如,在课程里他教大家用Python绘制雪景图片,这招最近在抖音和知乎都很红! 在他的课里你能学会的是一种资深程序员的思维模式,而不是单纯的语法规则。 嵩天老师凭爱和本事卖“安利”,让普通人也能很快get到Python的的奥义。学生满天下的他,就是coding届的邓布利多。 入门时间按天算的Python, 是机器学习圈最流行的语言 嵩天老师在MOOC开设Python课程的时候(2016年),Python语言在全球范围内的使用频率也越来越高。Python甚至进入了浙江信息技术高考,因为有Python,00后可能每个人都会编程了。这种全民狂热的背后有很重要一个原因是——人工智能的风口来了。 根据数据平台 Kaggle 发布的2017 机器学习及数据科学调查报告,在工具语言使用方面,Python是使用最多的语言。 也正是因为这种广泛使用,Python 社区异常活跃,目前周边算是非常成熟,针对不同的需求都有着自己比较受欢迎的库。 Python 这么厉害, 是要替代C/C++的地位吗? 前面介绍的“吃鸡”,虽然底层是用C/C++写的 ,但是Python可以调用AI接口,而且代码非常简单,不是说C++写不了上层逻辑,而是Python入门时间按天算的,C++入门时间按年算的…… 请问,在有限的生命里,你打算学哪种编程语言?在线等,挺急的。 实际上在嵩天老师的Python系列课程里,有很多关于深度学习、人工智能、数据处理等内容。如果你也和那个中文系的妹子一样,刷了嵩天老师的MOOC,是他的迷妹/迷弟的话,嵩天老师在两年多的网络教学实战后,又推出了一门进阶课程——《Python 应用基础》微专业。 如果没有刷过、甚至从来没有接触过编程也不用担心,《Python应用基础》学员群有系统预习资料、会带你从0开始预习,加入即可告别入门难的烦恼。 原文发布时间为:2018-09-11 本文来自云栖社区合作伙伴“机器学习算法与Python学习”,了解相关信息可以关注“机器学习算法与Python学习”。

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

云生态:云计算棋局中的“胜负手”

浪潮云数据中心全国合作伙伴大会(IPF2017)刚刚落下帷幕,大会汇聚了来自全国各地的行业ISV、SI、分销商等近2000家合作伙伴。此次生态大会的一个核心议题是深化合作伙伴生态2.0,赋能伙伴转型升级。 合作伙伴生态2.0是浪潮在云计算、大数据时代的伙伴体系建设新模式,随着浪潮合作伙伴生态的深耕,伙伴生态广度在扩大的同时,合作深度也在持续递进。浪潮与合作伙伴之间将从原有的基于商务和销售的供应链模式,转变为基于研发和产品层面合作的价值链模式,从而实现横向扩展生态圈、垂直整合生态链。 在云计算领域,行业内各种生态主张更是此起彼伏,每个云计算厂商都在"跑马圈地"打造生态。为什么"生态"会在最近两年成为各大云计算厂商频繁提及的热词?其背后的根源在于云计算产业环境的变化。 应用繁荣下的生态共识 从概念探索期到技术落地期,如今随着云平台的成熟和各式终端设备的层出不穷,云计算正进入一个新的"应用繁荣期",其重要标识就是云计算的重心从以提供云基础设施为主转为支撑云应用为主,针对不断涌现的用户需求,如何快速、灵活、低成本的开发相应应用成为云计算的重点所在,API经济开始兴起。 当用户对云计算的关注点从基础架构延展到应用开发时,势必会对提供云计算解决方案的厂商提出更高的要求,这也促使IaaS、PaaS厂商与应用开发者的生态融合,即IaaS与PaaS厂商致力于提供一个架构灵活、敏捷开发、安全可靠的云计算基础平台,而众多的开发者和SaaS厂商则致力提供种类丰富的应用。 事实上,近两年浪潮、阿里等公司大肆强调云生态的根本原因,就在于应用繁荣所带来的生态环境改变。如果将云计算看作一个星球,那么IaaS与PaaS厂商就是这个星球的土壤、空气与水,而开发者和SaaS厂商则是种类各异的生命形态。没有前者,整个星球将不复存在;没有后者,整个星球则死气沉沉,失去发展的可能。 行业云落地,生态使然 从用户视角纵观云计算的变化,从尝鲜虚拟化,试水云数据中心到近来的混合云、超融合、行业云。这其中反应出了从满足IT需求向更好的承载业务需求的转变,从实现IT价值最大化向创造更多业务价值的终极目标转变。各行各业都在思考如何在数字化转型的过程中催生新的活力,只有端、云业务协同,企业数字化重构才能成为可能。 需要指出的是很多云计算厂商经常提及三种云(共有、私有、混合),并且喜欢用不同的模式来套具体用户,实际上这种模式和用户实际需要的行业场景相并不完全匹配。尤其是在中国的行业云市场需求有着更独特的特点,更多行业用户对于云化业务模式的需求已经向商业与技术深度融合的方向发展,按照行业+云=行业云这种简单粗暴的方式去解读行业云的落地可能会给用户业务带来削足适履的违和感。 据此理解,站在云计算厂商视角,推动行业云的落地,从产品技术上,需要提供更加开放、敏捷的架构模型,与南北向技术伙伴保持高效的融合联动机制,以适配差异化的业务需求;从支撑业务角度,需要与SV、SI深度合作,提供针对性的行业解决方案;从运维、运营角度,需要广泛联合垂直行业或本地化产品方案服务商,为用户提供一站式、贴身式的运维、运营服务能力。 云图2.0持续深化 "十百千计划"+"0云风暴2.0"双线并进 2017年,浪潮明确未来将聚焦智慧计算,浪潮定义的智慧计算以云计算为基础平台,大数据为认知方法,深度学习为优化工具,秉承开放,融合的理念,与生态伙伴共同拥抱智慧世界。生态伙伴是推动构建未来智慧世界的重要一环,而云图计划正是云计算生态的重要承载。 在浪潮云图计划的构想中,将通过与业界优秀公司的"泛联合",从技术的研究与研发、方案的验证与优化、服务的提升及响应三个层面,设置三种不同类型的合伙人:技术合伙人指向开放API接口,联合产品开发、产品认证和安全测试;方案合伙人意在联合行业SI、ISV,验证、优化行业一体化交付解决方案;服务合作人则是联合SI及区域合作伙伴,共同为用户提供云计算本地化可持续的服务。 在今年的IPF上,浪潮宣布启动"InCloud Lab"建设计划,为期3年,总投资达2亿美元,Incloud Lab将成为"浪潮+合作伙伴" 联合孵化方案、联合验证的统一技术平台, 2019年底,参与Incloud Lab的合作伙伴总数将增加到200家。 •面向云计算生态,浪潮"云图计划"将进一步加大力度实施,"云图计划"在2017年要完成"十千百"目标,即开发10个行业云方案、发展100家合作伙伴、认证1000名工程师,扩大云计算生态圈规模。 •十大行业解决方案:以"云图"InCloud Lab为依托,联合Key ISV/SI开发医疗、政务、金融、通信、能源、教育十大行业解决方案;率先在方案验证优化测试中心进行各行业解决方案的用例测试,推动稳定、成熟的行业解决方案在用户生产环境的实测与落地。 •百家生态合作伙伴:通过整合行业云产业链上下游合作伙伴,从技术的研究与研发、行业方案的验证与优化、服务的提升和响应三个层面,围绕企业级用户构建融合、开放、安全、共赢的云生态版图,为企业级用户提供完整的数据中心云化解决方案。 千名工程师认证:认证1000名IICE/IICP专业工程师,提升本地服务能力; 此外继2016年IPF大会推出"0"云风暴以来,已累计产生了68000台ICS虚拟化产品的服务器出厂预装,在为用户提供上云便利的同时,也为广大合作伙伴提供了大量的销售机会。2017年"0"云风暴将开启2.0模式, 在ICS免费试用的基础上,增加集群环境的试用服务,同时"0"云专属服务体系和赋能体系实现了全国范围的覆盖。让中小企业云化的企业快速地享受到上门的便利,让合作伙伴真正地做到第一步开始进行云计算销售的尝试,而不需要培训复杂的能力,在云计算有行业云的解决方案,让合作伙伴全面拥抱产品。 浪潮云计算产品部总经理蒋永昌表示,"0云风暴"项目对我们的影响,可以用"触动"二字概况。一方面 "0云风暴"项目带给伙伴们从单一产品到方案经营模式的转变,另一方面来自用户的肯定,是让我们持续推进"0"云风暴的动力,让用户体验更便捷、更省心的服务,和合作伙伴合作、共赢是我们推进云图生态的宗旨,初心不改,方得始终。 原文发布时间为:2017年5月8日 本文来自云栖社区合作伙伴至顶网,了解相关信息可以关注至顶网。

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

贼厉害,手撸的 SpringBoot 缓存系统,性能杠杠的!

点击上方蓝色字体,选择“标星公众号” 优质文章,第一时间送达 作者|白楠楠 来源 | urlify.cn/YnMJ3q 66套java从入门到精通实战课程分享 缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。 本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统。 一、通用缓存接口 1、缓存基础算法 (1)、FIFO(First In First Out),先进先出,和OS里的FIFO思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。(2)、LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。(3)、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函数返回对象可为空*@paramexpireTime过期时间(单位:毫秒)可为空**/<TextendsObject>Tget(Stringkey,Function<String,T>function,LongexpireTime);/***查询缓存**@paramkey缓存键不可为空*@paramfunction如没有缓存,调用该callable函数返回对象可为空*@paramfuncParmfunction函数的调用参数*@paramexpireTime过期时间(单位:毫秒)可为空**/<TextendsObject,MextendsObject>Tget(Stringkey,Function<M,T>function,MfuncParm,LongexpireTime);/***设置缓存键值**@paramkey缓存键不可为空*@paramobj缓存值不可为空**/<TextendsObject>voidset(Stringkey,Tobj);/***设置缓存键值**@paramkey缓存键不可为空*@paramobj缓存值不可为空*@paramexpireTime过期时间(单位:毫秒)可为空**/<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函数返回对象可为空*@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);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缓存值不可为空*@paramexpireTime过期时间(单位:毫秒)可为空**/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真的是非常非常中规中矩的语言,你需要不断折腾并熟悉常见的开源中间件和工具,开源的轮子实在是太丰富,多尝试几个,实践出真知。 粉丝福利:实战springboot+CAS单点登录系统视频教程免费领取 👇👇👇 👆长按上方微信二维码2 秒 即可获取资料 感谢点赞支持下哈 本文分享自微信公众号 - java1234(gh_27ed55ecb177)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

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

CentOS7手动安装OpenStack Pike版--(neutron)

OpenStack安装指南_Mitakahttp://down.51cto.com/data/2331199 Openstack管理手册-Newton版-CentOS7.2http://down.51cto.com/data/2331201 #Configure neutron mysql -uroot -ppasswd123 -e "CREATE DATABASE neutron" mysql -uroot -ppasswd123 -e "GRANT ALL PRIVILEGES ON neutron.TO 'neutron'@'localhost' IDENTIFIED BY 'passwd123'" mysql -uroot -ppasswd123 -e "GRANT ALL PRIVILEGES ON neutron.TO 'neutron'@'%' IDENTIFIED BY 'passwd123'" openstack user create --domain default --password passwd123 neutron openstack role add --project service --user neutron admin openstack service create --name neutron --description "OpenStack Networking" network openstack endpoint create --region RegionOne network publichttp://controller:9696 openstack endpoint create --region RegionOne network internalhttp://controller:9696 openstack endpoint create --region RegionOne network adminhttp://controller:9696 Networking Option 2: Self-service networks Edit the /etc/neutron/neutron.conf openstack-config --set /etc/neutron/neutron.conf database connection mysql+pymysql://neutron:passwd123@controller/neutron openstack-config --set /etc/neutron/neutron.conf DEFAULT core_plugin ml2 openstack-config --set /etc/neutron/neutron.conf DEFAULT service_plugins router openstack-config --set /etc/neutron/neutron.conf DEFAULT allow_overlapping_ips true openstack-config --set /etc/neutron/neutron.conf DEFAULT transport_url rabbit://openstack:passwd123@controller openstack-config --set /etc/neutron/neutron.conf DEFAULT auth_strategy keystone openstack-config --set /etc/neutron/neutron.conf keystone_authtoken auth_urihttp://controller:5000 openstack-config --set /etc/neutron/neutron.conf keystone_authtoken auth_urlhttp://controller:35357 openstack-config --set /etc/neutron/neutron.conf keystone_authtoken memcached_servers controller:11211 openstack-config --set /etc/neutron/neutron.conf keystone_authtoken auth_type password openstack-config --set /etc/neutron/neutron.conf keystone_authtoken project_domain_name default openstack-config --set /etc/neutron/neutron.conf keystone_authtoken user_domain_name default openstack-config --set /etc/neutron/neutron.conf keystone_authtoken project_name service openstack-config --set /etc/neutron/neutron.conf keystone_authtoken username neutron openstack-config --set /etc/neutron/neutron.conf keystone_authtoken password passwd123 openstack-config --set /etc/neutron/neutron.conf DEFAULT notify_nova_on_port_status_changes true openstack-config --set /etc/neutron/neutron.conf DEFAULT notify_nova_on_port_data_changes true openstack-config --set /etc/neutron/neutron.conf nova auth_urlhttp://controller:35357 openstack-config --set /etc/neutron/neutron.conf nova auth_type password openstack-config --set /etc/neutron/neutron.conf nova project_domain_name default openstack-config --set /etc/neutron/neutron.conf nova user_domain_name default openstack-config --set /etc/neutron/neutron.conf nova region_name RegionOne openstack-config --set /etc/neutron/neutron.conf nova project_name service openstack-config --set /etc/neutron/neutron.conf nova username nova openstack-config --set /etc/neutron/neutron.conf nova password passwd123 openstack-config --set /etc/neutron/neutron.conf oslo_concurrency lock_path /var/lib/neutron/tmp Edit the /etc/neutron/plugins/ml2/ml2_conf.ini openstack-config --set /etc/neutron/plugins/ml2/ml2_conf.ini ml2 type_drivers flat,vlan,vxlan openstack-config --set /etc/neutron/plugins/ml2/ml2_conf.ini ml2 tenant_network_types vxlan openstack-config --set /etc/neutron/plugins/ml2/ml2_conf.ini ml2 mechanism_drivers linuxbridge,l2population openstack-config --set /etc/neutron/plugins/ml2/ml2_conf.ini ml2 extension_drivers port_security openstack-config --set /etc/neutron/plugins/ml2/ml2_conf.ini ml2_type_flat flat_networks provider openstack-config --set /etc/neutron/plugins/ml2/ml2_conf.ini ml2_type_vxlan vni_ranges 1:1000 openstack-config --set /etc/neutron/plugins/ml2/ml2_conf.ini securitygroup enable_ipset true Edit the /etc/neutron/plugins/ml2/linuxbridge_agent.ini openstack-config --set /etc/neutron/plugins/ml2/linuxbridge_agent.ini linux_bridge physical_interface_mappings provider:ens34 openstack-config --set /etc/neutron/plugins/ml2/linuxbridge_agent.ini vxlan enable_vxlan true openstack-config --set /etc/neutron/plugins/ml2/linuxbridge_agent.ini vxlan local_ip 192.168.137.145 openstack-config --set /etc/neutron/plugins/ml2/linuxbridge_agent.ini vxlan l2_population true openstack-config --set /etc/neutron/plugins/ml2/linuxbridge_agent.ini securitygroup enable_security_group true openstack-config --set /etc/neutron/plugins/ml2/linuxbridge_agent.ini securitygroup firewall_driver neutron.agent.linux.iptables_firewall.IptablesFirewallDriver Edit the /etc/neutron/l3_agent.ini openstack-config --set /etc/neutron/l3_agent.ini DEFAULT interface_driver linuxbridge Edit the /etc/neutron/dhcp_agent.ini openstack-config --set /etc/neutron/dhcp_agent.ini DEFAULT interface_driver linuxbridge openstack-config --set /etc/neutron/dhcp_agent.ini DEFAULT dhcp_driver neutron.agent.linux.dhcp.Dnsmasq openstack-config --set /etc/neutron/dhcp_agent.ini DEFAULT enable_isolated_metadata true Edit the /etc/neutron/metadata_agent.ini openstack-config --set /etc/neutron/metadata_agent.ini DEFAULT nova_metadata_host controller openstack-config --set /etc/neutron/metadata_agent.ini DEFAULT metadata_proxy_shared_secret openstack Edit the /etc/nova/nova.conf openstack-config --set /etc/nova/nova.conf neutron urlhttp://controller:9696 openstack-config --set /etc/nova/nova.conf neutron auth_urlhttp://controller:35357 openstack-config --set /etc/nova/nova.conf neutron auth_type password openstack-config --set /etc/nova/nova.conf neutron project_domain_name default openstack-config --set /etc/nova/nova.conf neutron user_domain_name default openstack-config --set /etc/nova/nova.conf neutron region_name RegionOne openstack-config --set /etc/nova/nova.conf neutron project_name service openstack-config --set /etc/nova/nova.conf neutron username neutron openstack-config --set /etc/nova/nova.conf neutron password passwd123 openstack-config --set /etc/nova/nova.conf neutron service_metadata_proxy true openstack-config --set /etc/nova/nova.conf neutron metadata_proxy_shared_secret = openstack ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini su -s /bin/sh -c "neutron-db-manage --config-file /etc/neutron/neutron.conf \ --config-file /etc/neutron/plugins/ml2/ml2_conf.ini upgrade head" neutron systemctl restart openstack-nova-api.service systemctl enable neutron-server.service \ neutron-linuxbridge-agent.service neutron-dhcp-agent.service \ neutron-metadata-agent.service systemctl start neutron-server.service \ neutron-linuxbridge-agent.service neutron-dhcp-agent.service \ neutron-metadata-agent.service systemctl status neutron-server.service \ neutron-linuxbridge-agent.service neutron-dhcp-agent.service \ neutron-metadata-agent.service systemctl enable neutron-l3-agent.service systemctl start neutron-l3-agent.service systemctl status neutron-l3-agent.service 本文转自 OpenStack2015 博客,原文链接: http://blog.51cto.com/andyliu/2069166 如需转载请自行联系原作者

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

CentOS7手动安装OpenStack Pike版--(horizon)

OpenStack安装指南_Mitakahttp://down.51cto.com/data/2331199 Openstack管理手册-Newton版-CentOS7.2http://down.51cto.com/data/2331201 #Configure horizon Edit the /etc/openstack-dashboard/local_settings OPENSTACK_HOST = "controller" ALLOWED_HOSTS = ['*', ] SESSION_ENGINE = 'django.contrib.sessions.backends.cache' CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': 'controller:11211', }, } OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_HOST OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True OPENSTACK_API_VERSIONS = { "data-processing": 1.1, "identity": 3, "image": 2, "volume": 2, "compute": 2, } OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'Default' OPENSTACK_KEYSTONE_DEFAULT_ROLE = "user" TIME_ZONE = "Asia/Shanghai" systemctl restart httpd.service memcached.service controller节点安装完成,访问Dashboardhttp://192.168.137.145/dashboard/ 本文转自 OpenStack2015 博客,原文链接: http://blog.51cto.com/andyliu/2069169 如需转载请自行联系原作者

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

开源手游服务器引擎Scut 助力快速开发

Scut是一个开源、免费、稳定、快速开发的游戏服务器引擎,支持开发人员使用Python脚本语言或C#语言开发,底层采用C#编写,基于MVC框架思想设计,开发人员只需要关注如何定义数据实体类及属性,不再需要关注多据库(MSSQL、MySql等)及表设计,Scut会帮助您自动检测生成相应数据库的表结构。 作为游戏服务器引擎,Scut具有8大优势特性。支持Windows/Linux多平台部署;支持MSSQL、Mysql等多数据库;支持HTTP、Socket多通讯协议;支持多服务器耦合分布式架构;支持使用Python脚本开发,维护时热更新;支持数据临时或永久的缓存,降低数据库压力,数据可同步或延迟更新。此外,更具有“低门槛,开发容易,提供丰富的游戏模块中间件及例程”,以及“低内存和CPU占用,24小时不使用的对象自动交换至数据库,并从内存移除”等优点。 游戏服务器引擎Scut(www.scutgame.com)还提供了丰富的AIP和成熟的游戏模块中间件,助力你的游戏服务器应用快速开发。 服务器层次图 服务器支持同时提供Http和Socket两种通讯接入方式;使用二进制协议,可以通过“协议生成器”工具快速定义客户端与服务器的接口通讯协议,并同时生成客户端与服务器两端的接口通讯代码。 提供的游戏模块中间件可以方便快速构建应用,特别对繁琐的渠道登录和充值的SDK模块接入进入封装,简单的配置即可完成接入。 游戏数据处理是最重要、最复杂、也是最容易出错的部分,框架封装了对数据库数据的读取、更新、删除和表的创建维护等管理,及支持MSSQL、Mysql等多数据库;让开发人员不必再关心数据库,只需要定义自己的数据实体类及属性;另外还提供数据缓存管理,可以对使用频繁数据进行缓存,在一定时间内未使用的缓存置换出去,并更新到数据库;缓存中改动的数据会间隔10分种异步更新到数据库(也可以时时更新)。 服务器架构示意图 服务器拓扑结构示意图 精简的服务器结构示意图

资源下载

更多资源
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等操作系统。

用户登录
用户注册