首页 文章 精选 留言 我的

精选列表

搜索[手游加固],共7841篇文章
优秀的个人博客,低调大师

真香,手撸的 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源创计划”,欢迎正在阅读的你也加入,一起分享。

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

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

浪潮云数据中心全国合作伙伴大会(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日 本文来自云栖社区合作伙伴至顶网,了解相关信息可以关注至顶网。

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

腾讯云国际站代理商:CVM服务器安全加固配置指南

公网环境下自动化扫描、密码爆破攻击十分普遍。出海企业的腾讯云国际 CVM 直接暴露公网,如果基线配置不完善,极易出现账号泄露、主机被入侵。本文完整讲解腾讯云国际版 CVM 三大安全基线:禁用 SSH 密码登录、运维堡垒机部署、CWPP 主机安全检测,帮助运维人员收敛攻击面,适配 Linux 与 Windows 混合业务场景。

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

积木报表 JimuReport v1.6.2-GA2 版本发布 — 高危 SQL 漏洞安全加固版本

项目介绍 一款免费的数据可视化报表,含报表和大屏设计,像搭建积木一样在线设计报表!功能涵盖,数据报表、打印设计、图表报表、大屏设计等! Web 版报表设计器,类似于 excel 操作风格,通过拖拽完成报表设计。 秉承 “简单、易用、专业” 的产品理念,极大的降低报表开发难度、缩短开发周期、节省成本、解决各类报表难题。 领先的企业级 Web 报表软件,采用纯 Web 在线技术,专注于解决企业报表快速制作难题。 当前版本:v1.6.2-GA2 | 2023-09-12 #升级内容 重点解决 SQL 漏洞被攻击等安全问题!本次版本进行了非常大重构,重构了权限机制并彻底重写了 SQL 执行逻辑,解决了 SQL 漏洞风险;并处理了上个版本已知严重 Bug; ::: 重要的事情只说一遍,必须升级,不然你会被攻击 ::: #新版规则变化 1、多租户的配置方式变更为:saasMode 2、新增低代码开发模式 lowCodeMode:prod,完全禁止在线报表设计能力,彻底避免被攻击 3、默认报表预览地址必须带 token,只能通过报表分享连接给他们访问报表 集成依赖 <dependency> <groupId>org.jeecgframework.jimureport</groupId> <artifactId>jimureport-spring-boot-starter</artifactId> <version>1.6.2-GA2</version> </dependency> 最新依赖还未上传 maven 官仓,下载失败请先配置JEECG 的 Maven 私服。 #升级日志 #升级权限内容:通过以下几个维度保障报表安全 1、增加低代码开发模式配置 jeecg.jmreport.firewall.lowCodeMode: prod 发布模式下会关闭所有报表设计相关接口,普通用户只能访问报表不能做任何报表修改,彻底解决被攻击风险 为了便于线上报表临时维护,拥有角色 "admin"、"lowdeveloper" 的用户,可以拥有设计权限 2、敏感接口,增加角色权限控制 容易被攻击的敏感接口默认加了角色权限控制,拥有角色 "admin","lowdeveloper","dbadeveloper" 的用户,可以访问这些接口 敏感接口如下: a、数据预览接口 b、数据源连接测试是否准确接口 3、增加数据隔离配置 jmreport.saasMode:created 线上发布请按照创建人或者租户实现数据隔离,保证他人数据安全 created:按照创建人隔离、tenant:按照租户隔离 4、增加数据源安全配置 jeecg.jmreport.firewall.dataSourceSafe: true 开启数据源安全后,不允许使用平台数据源、SQL解析加签并且不允许查询数据库 5、重写了sql参数拼接的写法,全部换成占位符方式,防止被攻入的可能 6、进一步加强了sql注入检查算法,通过深度解析SQL,检查是否存在攻击函数等 详细配置参数如下: jeecg : jmreport: #多租户模式,默认值为空(created:按照创建人隔离、tenant:按照租户隔离) (v1.6.2+ 新增) saasMode: created # 平台上线安全配置(v1.6.2+ 新增) firewall: # 数据源安全 (开启后,不允许使用平台数据源、SQL解析加签并不允许查询数据库) dataSourceSafe: true # 低代码开发模式(dev:开发模式,prod:发布模式—关闭在线报表设计功能,分配角色admin、lowdeveloper可放开限制) lowCodeMode: prod 特殊场景 如果某个人可以在测试环境下设计报表,但是不能在发布环境下设计报表,只分配角色dbadeveloper即可,当然测试环境下需要把lowCodeMode改成 dev。 #升级修复 ISSUES 当单元格设置格式为数值是 0 值不显示#1936 打印出现空白页#1924 使用统计函数 = DBSUM,导致预览页面展示空白#1806 单元格为数值类型,若为值 0 或者为空,控制台报错#1940 在设计报表使用数据图表设置为 SQL 数据集无法运行更新并保存#1629 横向分组使用右侧输入值无法预览#1864 在 W 列之后添加 compute 计算函数导致整个报表都无法显示#1866 自定义函数参数中有单元格取值和自定义参数时,单元格参数获取为空#1895 导入报表 sql,界面没有显示,控制台提示 json 解析错误#1482 参数的宽度怎么调整#1631 报表钻取问题,跳转参数设置问题,条件不生效问题#1604 SQL 解析空表时报 500#1658 api 数据集,重新解析时最后一列 参数配置 的数据会被清除掉#1485 关于数据集格式化后导出保留两位小数点,不足补零的问题#1834 springboot 2.3.5-RELAESE 预览报表界面接口 jmreport/getQueryInfo 返回为空导致一直加载#1660 excel 交叉报表导出报错#1696 Excel 导出当数值个位数时 (0-9) 会为文本格式#1575 多数据集与分栏功能共同使用时报 NTP#1587 Excel 导出后数字为 0 的表格显示类型不正确#1452 报表数据超过 1000 多条时,导出失败#1749 关于报表页面展示与导出 excel 样式不一致问题的补充#1646 合并单元格并设置单元格格式为条形码或二维码时,在某些样式下无法导出 pdf#1649 关于数据解析的问题#1521 日期时间类型数据,导出为 pdf,格式不一致#1942 自带的分页查询接口返回 pageNo 不对#1578 api 解析报表参数传时间默认值问题#1600 API 自定义查询条件,API 被调用多次#1325 报表钻取后 返回上一页发现 日期查询条件的日期变成上一天了#1886 升级版本后手写分页失效#1453 页码显示有误#1893 固定表头打印#1941 版本 1.6.0 图表显示异常,数据已经提取,但部分内容未显示完全,呈现空白#1921 循环块中插入二维码打印异常分页#1655 1.5.8 升级到 1.6.1 后,预览显示不全,打印预览正常#1931 表单与预览不一致#1944 模板设置无边框,导出 Excel 还是有边框#1512 二维码生成的容错级别#1957 jeecgboot3.5.3 存在未授权 sql 注入(布尔盲注绕过)#5311 数值为 0 的单元格打印时变成空值#1972 字典 code sql 包含系统变量时,报表该列数据无法正常翻译为显示值#1984 #代码下载 https://github.com/jeecgboot/JimuReport https://gitee.com/jeecg/JimuReport #技术文档 体验官网:http://jimureport.com 快速集成文档 :https://help.jeecg.com/jimureport/quick.html 技术文档:https://help.jeecg.com/jimureport 为什么选择 JimuReport? 永久免费,支持各种复杂报表,并且傻瓜式在线设计,非常的智能,低代码时代,这个是你的首选! 采用 SpringBoot 的脚手架项目,都可以快速集成 Web 版设计器,类似于 excel 操作风格,通过拖拽完成报表设计 通过 SQL、API 等方式,将数据源与模板绑定。同时支持表达式,自动计算合计等功能,使计算工作量降低 开发效率很高,傻瓜式在线报表设计,一分钟设计一个报表,又简单又强大 支持 ECharts,目前支持 28 种图表,在线拖拽设计,支持 SQL 和 API 两种数据源 支持分组、交叉,合计、表达式等复杂报表 支持打印设计(支持套打、背景打印等)可设置打印边距、方向、页眉页脚等参数 一键快速打印 同时可实现套打,不动产证等精准、无缝打印 大屏设计器支持几十种图表样式,可自由拼接、组合,设计炫酷大屏 可设计各种类型的单据、大屏,如出入库单、销售单、财务报表、合同、监控大屏、旅游数据大屏等 报表设计效果 报表设计器(专业一流 数据可视化,解决各类报表难题) 报表设计器(完全在线设计,简单易用) 打印设计(支持套打、背景打印) 数据报表(支持分组、交叉,合计等复杂报表) 图形报表(目前支持 28 种图表) 数据报表斑马线 大屏设计效果 仪表盘设计器

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

网络安全保险加固企业风险“防火墙”

业内人士认为,随着数字(网络)技术的发展,社会生产、生活越来越依赖互联网,数字时代最大的风险就是数字风险,在数字安全服务供应商的推动下,网络安全保险获得了快速发展。 信息“漏洞”催生险种落地 网络数据信息泄露,在当下屡见不鲜,近期斯坦福大学个人和财务信息泄露、红杉资本投资者信息泄露等事件亦引发广泛担忧。 除了数据泄露之外,勒索病毒亦是数字化转型企业的安全“劲敌”。上海信息安全行业协会副主任张威便介绍称,其曾遇到过形形色色勒索病毒事件,部分企业被勒索数万元甚至上百万元。 层出不穷、形形色色的网络安全风险,对数据海洋中航行的大小企业“虎视眈眈”。迈克菲的研究数据显示,中国平均每年由于网络安全问题造成的损失高达600亿美元,居亚洲首位、全球第二。 如何应对巨额损失?如何防控巨量风险?北京商报记者发现,日前有机构推出网络安全风险解决方案。其中,网络安全保险大放异彩。 如创新技术公司源堡科技便在日前提出“防”+“保”综合解决方案。企业可以针对自身面临的网络安全风险得到相应的网络安全保险保障,同时源堡科技还为投保企业提供“7×24小时持续风险监测和预警”服务,并建立外部风险持续监测和快速应急响应能力:9大维度,监测200+项指标,高危风险分钟级警告。 在这张网络安全“天网”联结点上,除科技企业外,中国人寿、阳光保险、中再集团、中国人保均有活跃。而放眼国际,今年3月,Alphabet旗下谷歌云业务亦与安联保险公司和慕尼黑再保险公司展开合作,将把有关客户网络风险的数据与保单相整合。 所谓“网络安全保险”,保障的是由于网络安全事件给企业带来的直接财产损失,以及第三方客户提出的索赔责任。据首都经贸大学保险系副主任李文中介绍,总体上看,当前网络安全保险主要为客户提供的安全保障包括数据(系统)丢失恢复及相关费用损失、电子账户(支付)损失、网络敲诈损失、名誉损失、利润损失和第三者责任。 “这些保险责任已经覆盖了人们在使用互联网时面临的主要风险,因此对于保障人们安全使用互联网,推动数字技术进步具有重要意义。”对此,李文中如是评价。 初生市场挑战不可小觑 那么,网络安全保险目前还面临着哪些挑战? 据李文中介绍,首先,网络安全保险刚刚兴起,而且数字(网络)风险的形态处于快速变化之中,使得保险公司难以为网络安全保险产品准确定价。 相关资料显示,相较国际上发展40年趋于成熟的市场,网络安全保险近几年才逐渐在国内兴起,仍处于起步阶段。银保监会公开数据显示,目前国内共有21家中资保险公司能够提供网络安全保险的相关产品和承保能力,市场上目前仅有38款保险产品。 其次,李文中指出,保险公司的数字(网络)安全技术人员,特别是高水平的安全工程师严重不足,一方面难以为客户提供有效的事前风险防范管理服务,另一方面经营中也面临着较高的保险欺诈风险。 “再次,目前除了电子账户外,其他数字资产价值还难以被准确评估,导致网络安全保险尚不能为被保险因数据丢失引起的数字资产损失提供补偿,而只能补偿数据恢复重置的费用。最后,一旦发生大规模网络安全攻击事件,保险公司有可能面临巨额保险索赔,威胁保险公司的财务安全。”李文中如是表示。 “对于保险公司来说,经营网络安全风险一定要量力而行,做好经营风险管控。”关于网络安全保险经营险企,李文中给出了如是忠告。 清华大学五道口金融学院中国保险和养老金研究中心研究总监朱俊生亦指出,网络安全风险可能会演化成一种巨灾风险,责任程度可能会很大。对此,他表示:“保险公司如何进行风险分散,特别是通过再保险机制进行分散亦是非常重要的。” 潜在市场空间巨大 机遇与挑战并存,网络安全保险的发展亦不例外。 李文中便指出,当前我国在新一代互联网技术应用上居于全球领先地位,“互联网+”已经渗透到生产、生活的方方面面,国家一直鼓励和支持相关技术创新,以5G为代表的新基建是国家“十四五”规划的重要内容。这为网络安全保险在我国的发展提供了难得的机遇。 业内人士亦指出,法制环境逐渐完善亦是网络安全保险发展的重要机遇。据介绍,随着《中华人民共和国网络安全法》的正式实施,网络安全的监管不断加强,国家首次建立了个人信息保护制度,并对网络运营者提出了一系列安全管理要求。《网络安全等级保护条例》的颁布,强化了网络安全在操作性、指导性和强制性上的保护力度。同时,我国的《数据安全法》《个人信息保护法》《网络安全等级保护条例》《关键信息基础设施安全保护条例》等相关法律政策与标准规范的起草与出台。 “随着法律法规制度的完善,我国网络安全市场将进一步加速发展,网络安全保险也将最大限度地发挥保险的经济补偿功能,解决社会民众的数据安全风险保障,为个人隐私保护建立安全屏障。”对此,该人士如是评价。 同时,“十四五”规划加速数字化中国,亦为网络安全保险提供前所未有的发展机遇。上述人士指出:“网络安全保险作为数字化过程中防范风险损失的重要一环,其保险功能的发挥有助于降低中国数字化进程中的安全风险,进一步健全企业网络安全的风险管理体系,保障企业的网络信息安全。” 此外,业内人士亦指出,新一代信息技术为金融业高质量发展提供了重要支撑,保险业要加大“保险+科技”的升级力度,在产品、营销、理赔、管理等方面充分利用网络技术实现转型升级。作为技术支撑力量,保险科技公司应提供专业的技术保障,以解决数字泄露、个人信息被窃取等问题,进一步加强网络安全保险的转型升级。 “网络安全保险潜在市场空间非常大,如果风险管理做的相对比较到位,风控做得较好,背后也有盈利增长的潜力,越来越多公司将关注这一领域。”对于网络安全保险市场的发展前景,朱俊生表达了乐观态度。

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

贼厉害,手撸的 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源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

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应用均可从中受益。

Sublime Text

Sublime Text

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

用户登录
用户注册