HUND 中文站
JetCache 缓存框架的使用及源码解析(推荐)
发布日期:2025-01-02 12:22    点击次数:113
一、简介 JetCache是一个基于Java的缓存系统封装,提供统一的API和注解来简化缓存的使用。 JetCache提供了比SpringCache更加强大的注解,可以原生的支持TTL、两级缓存、分布式自动刷新,还提供了Cache接口用于手工缓存操作。 当前有四个实现:RedisCache、RedisLettuceCache、CaffeineCache、LinkedHashMapCache。 特性: 通过统一的API访问Cache系统通过注解实现声明式的方法缓存,支持TTL和两级缓存通过注解创建并配置Cache实例针对所有Cache实例和方法缓存的自动统计Key的生成策略和Value的序列化策略支持自定义配置分布式缓存自动刷新,分布式锁异步Cache API (使用Redis的Lettuce客户端时) 缓存类型: 本地 LinkedHashMap:使用LinkedHashMap做LUR方式淘汰Caffeine:基于Java8开发的提供了近乎最佳命中率的高性能的缓存库 远程(访问Redis的客户端) Redis:使用Jedis客户端,Redis官方首选的Java客户端RedisSpringData:使用SpringData访问Redis(官网未作介绍)RedisLettuce:使用Lettuce客户端,一个高性能基于Java的Redis驱动框架,支持线程安全的同步、异步操作,底层集成了Project Reactor,提供反应式编程,参考:关于SpringBoot整合redis使用Lettuce客户端超时问题 为什么使用缓存? 在高并发、大流量等场景下,降低系统延迟,缓解数据库压力,提高系统整体的性能,让用户有更好的体验。 使用场景 读多写少、不追求强一致性、请求入参不易变化 使用规范 选择了远程缓存请设置keyPrefix,保证存放至Redis的缓存key规范化,避免与其他系统出现冲突,例如这样设计:系统简称:所属名字:,这样存储到Redis的缓存key为:系统简称:所属名字:缓存key 选择了本地缓存请设置limit,全局默认设置了100,本地缓存的数据存放于内存,减轻内存的损耗,如果使用了Caffeine,缓存的key过多可能导致内存溢出 请勿滥用缓存注解,对于非必要添加缓存的方法我们尽量不使用缓存 二、如何使用 说明:以下使用方式是基于SpringBoot引入JetCache缓存框架的,如果不是SpringBoot工程,请参考JetCache官网使用 引入maven依赖 添加配置 配置说明 jetcache的全局配置 属性默认值说明jetcache.statIntervalMinutes0用于统计缓存调用相关信息的统计间隔(分钟),0表示不统计。jetcache.areaInCacheNametrue缓存实例名称cacheName会作为缓存key的前缀,2.4.3以前的版本总是把areaName加在cacheName中,因此areaName也出现在key前缀中。我们一般设置为false。jetcache.penetrationProtectfalse当缓存访问未命中的情况下,对并发进行的加载行为进行保护。 当前版本实现的是单JVM内的保护,即同一个JVM中同一个key只有一个线程去加载,其它线程等待结果。这是全局配置,如果缓存实例没有指定则使用全局配置。jetcache.enableMethodCachetrue是否使用jetcache缓存。jetcache.hiddenPackages无自动生成缓存实例名称时,为了不让名称太长,hiddenPackages指定的包名前缀会被截掉,多个包名使用逗号分隔。我们一般会指定每个缓存实例的名称。 本地缓存的全局配置 属性默认值说明jetcache.local.${area}.type无本地缓存类型,支持 linkedhashmap、caffeine。jetcache.local.${area}.limit100每个缓存实例存储的缓存数量的全局配置,仅本地缓存需要配置,如果缓存实例没有指定则使用全局配置,请结合实例的业务场景进行配置该参数。jetcache.local.${area}.keyConvertor无缓存key转换器的全局配置,支持的类型:fastjson。仅当使用@CreateCache且缓存类型为LOCAL时可以指定为none,此时通过equals方法来识别key。方法缓存必须指定keyConvertor。支持自定义转换器函数,可设置为:bean:beanName,然后会从spring容器中获取该bean。jetcache.local.${area}.expireAfterWriteInMillis无穷大本地缓存超时时间的全局配置(毫秒)。jetcache.local.${area}.expireAfterAccessInMillis0多长时间没访问就让缓存失效的全局配置(毫秒),仅支持本地缓存。0表示不使用这个功能。 远程缓存的全局配置 属性默认值说明jetcache.remote.${area}.type无连接Redis的客户端类型,支持 redis、redis.lettuce、redis.springdata。jetcache.remote.${area}.keyPrefix无保存至远程缓存key的前缀,请规范使用。jetcache.remote.${area}.keyConvertor无参考上述说明。jetcache.remote.${area}.valueEncoderjava保存至远程缓存value的编码函数,支持:java、kryo。支持自定义编码函数,可设置为:bean:beanName,然后会从spring容器中获取该bean。jetcache.remote.${area}.valueDecoderjava保存至远程缓存value的解码函数,支持:java、kryo。支持自定义解码函数,可设置为:bean:beanName,然后会从spring容器中获取该bean。jetcache.remote.${area}.expireAfterWriteInMillis无穷大远程缓存超时时间的全局配置(毫秒)。jetcache.remote.${area}.uri无redis节点信息。 上表中${area}对应@Cached和@CreateCache的area属性,如果注解上没有指定area,默认值是"default"。 关于缓存的超时时间: put等方法上指定了超时时间,则以此时间为准;put等方法上未指定超时时间,使用Cache实例的默认超时时间;Cache实例的默认超时时间,通过在@CreateCache和@Cached上的expire属性指定,如果没有指定,使用yml中定义的全局配置,例如@Cached(cacheType=local)使用jetcache.local.default.expireAfterWriteInMillis,如果仍未指定则是无穷大。 注解说明 如果需要使用jetcache缓存,启动类添加两个注解:@EnableCreateCacheAnnotation、@EnableMethodCache @EnableCreateCacheAnnotation 开启可通过@CreateCache注解创建Cache实例功能。 @EnableMethodCache 开启可通过@Cached注解创建Cache实例功能,初始化spring aop,注解说明: 属性默认值说明basePackages无jetcache需要拦截的包名,只有这些包名下的Cache实例才会生效orderOrdered.LOWEST_PRECEDENCE指定AOP切面执行过程的顺序,默认最低优先级modeAdviceMode.PROXYSpring AOP的模式,目前就提供默认值让你修改proxyTargetClassfalse无 @Cached 为一个方法添加缓存,创建对应的缓存实例,注解可以添加在接口或者类的方法上面,该类必须是spring bean,注解说明: 属性默认值说明area"default"如果在配置中配置了多个缓存area,在这里指定使用哪个area。name未定义指定缓存实例名称,如果没有指定,会根据类名+方法名自动生成。name会被用于远程缓存的key前缀。另外在统计中,一个简短有意义的名字会提高可读性。enabledtrue是否激活缓存。timeUnitTimeUnit.SECONDS指定expire的单位。expire未定义超时时间。如果注解上没有定义,会使用全局配置,如果此时全局配置也没有定义,则为无穷大。localExpire未定义仅当cacheType为BOTH时适用,为本地缓存指定一个不一样的超时时间,通常应该小于expire。如果没有设置localExpire且cacheType为BOTH,那么本地缓存的超时时间和远程缓存保持一致。cacheTypeCacheType.REMOTE缓存的类型,支持:REMOTE、LOCAL、BOTH,如果定义为BOTH,会使用LOCAL和REMOTE组合成两级缓存。localLimit未定义如果cacheType为LOCAL或BOTH,这个参数指定本地缓存的最大元素数量,以控制内存占用。如果注解上没有定义,会使用全局配置,如果此时你没有定义全局配置,则使用默认的全局配置100。请结合实际业务场景进行设置该值。serialPolicy未定义指定远程缓存VALUE的序列化方式,支持SerialPolicy.JAVA、SerialPolicy.KRYO。如果注解上没有定义,会使用全局配置,如果你没有定义全局配置,则使用默认的全局配置SerialPolicy.JAVA。keyConvertor未定义指定KEY的转换方式,用于将复杂的KEY类型转换为缓存实现可以接受的类型,支持:KeyConvertor.FASTJSON、KeyConvertor.NONE。NONE表示不转换,FASTJSON可以将复杂对象KEY转换成String。如果注解上没有定义,会使用全局配置。key未定义使用SpEL指定缓存key,如果没有指定会根据入参自动生成。cacheNullValuefalse当方法返回值为null的时候是否要缓存。condition未定义使用SpEL指定条件,如果表达式返回true的时候才去缓存中查询。postCondition未定义使用SpEL指定条件,如果表达式返回true的时候才更新缓存,该评估在方法执行后进行,因此可以访问到#result。 @CacheInvalidate 用于移除缓存,配置说明: 配置默认值说明area"default"如果在配置中配置了多个缓存area,在这里指定使用哪个area。name无指定缓存的唯一名称,一般指向对应的@Cached定义的name。key未定义使用SpEL指定key,如果没有指定会根据入参自动生成。condition未定义使用SpEL指定条件,如果表达式返回true才执行删除,可访问方法结果#result。删除缓存实例中key的元素。multifalse如果根据SpEL指定的key是一个集合,是否从缓存实例中删除对应的每个缓存。如果设置为true,但是key不是集合,则不会删除缓存。 @CacheUpdate 用于更新缓存,配置说明: 配置默认值说明area"default"如果在配置中配置了多个缓存area,在这里指定使用哪个area。name无指定缓存的唯一名称,一般指向对应的@Cached定义的name。key未定义使用SpEL指定key,如果没有指定会根据入参自动生成。value无使用SpEL指定value。condition未定义使用SpEL指定条件,如果表达式返回true才执行更新,可访问方法结果#result。更新缓存实例中key的元素。multifalse如果根据SpEL指定key和value都是集合并且元素的个数相同,则是否更新缓存实例中的对应的每个元素。如果设置为true,但是key不是集合或者value不是集合或者它们的元素的个数不相同,也不会更新缓存。 @CacheRefresh 用于自定刷新缓存,配置说明: 配置默认值说明refresh无刷新间隔stopRefreshAfterLastAccess未定义指定该key多长时间没有访问就停止刷新,如果不指定会一直刷新。refreshLockTimeout60秒类型为BOTH/REMOTE的缓存刷新时,同时只会有一台服务器在刷新,这台服务器会在远程缓存放置一个分布式锁,此配置指定该锁的超时时间。timeUnitTimeUnit.SECONDS指定refresh时间单位。 @CachePenetrationProtect 当缓存访问未命中的情况下,对并发进行的加载行为进行保护。 当前版本实现的是单JVM内的保护,即同一个JVM中同一个key只有一个线程去加载,其它线程等待结果,配置说明: 配置默认值说明valuetrue是否开启保护模式。timeout未定义其他线程的等待超时时间,如果超时则自己执行方法直接返回结果。timeUnitTimeUnit.SECONDS指定timeout时间单位。 @CreateCache 在Spring Bean中使用该注解可创建一个Cache实例,配置说明: 配置默认值说明area"default"如果在配置中配置了多个缓存area,在这里指定使用哪个area。name未定义指定缓存实例名称,如果没有指定,会根据类名+方法名自动生成。name会被用于远程缓存的key前缀。另外在统计中,一个简短有意义的名字会提高可读性。timeUnitTimeUnit.SECONDS指定expire的单位。expire未定义超时时间。如果注解上没有定义,会使用全局配置,如果此时全局配置也没有定义,则为无穷大。localExpire未定义仅当cacheType为BOTH时适用,为本地缓存指定一个不一样的超时时间,通常应该小于expire。如果没有设置localExpire且cacheType为BOTH,那么本地缓存的超时时间和远程缓存保持一致。cacheTypeCacheType.REMOTE缓存的类型,支持:REMOTE、LOCAL、BOTH,如果定义为BOTH,会使用LOCAL和REMOTE组合成两级缓存。localLimit未定义如果cacheType为LOCAL或BOTH,这个参数指定本地缓存的最大元素数量,以控制内存占用。如果注解上没有定义,会使用全局配置,如果此时你没有定义全局配置,则使用默认的全局配置100。请结合实际业务场景进行设置该值。serialPolicy未定义指定远程缓存VALUE的序列化方式,支持SerialPolicy.JAVA、SerialPolicy.KRYO。如果注解上没有定义,会使用全局配置,如果你没有定义全局配置,则使用默认的全局配置SerialPolicy.JAVA。keyConvertor未定义指定KEY的转换方式,用于将复杂的KEY类型转换为缓存实现可以接受的类型,支持:KeyConvertor.FASTJSON、KeyConvertor.NONE。NONE表示不转换,FASTJSON可以将复杂对象KEY转换成String。如果注解上没有定义,会使用全局配置。 使用示例 如上述所示 getValue方法会创建一个缓存实例,通过@Cached注解可以看到缓存实例名称cacheName为'JetCacheExampleService.getValue',缓存的有效时长为6小时,本地缓存的数量最多为50,缓存类型为BOTH(优先从本地缓存获取);通过@CacheRefresh注解可以看到会为该缓存实例设置一个刷新策略,刷新间隔为1小时,2个小时没访问后不再刷新,需要刷新的缓存实例会为其每一个缓存数据创建一个RefreshTask周期性任务;@CachePenetrationProtect注解表示该缓存实例开启保护模式,当缓存未命中,同一个JVM中同一个key只有一个线程去加载数据,其它线程等待结果。 updateValue方法可以更新缓存,通过@CacheUpdate注解可以看到会更新缓存实例'JetCacheExampleService.getValue'中缓存key为#user.userId的缓存value为#user。 deleteValue方法可以删除缓存,通过@CacheInvalidate注解可以看到会删除缓存实例'JetCacheExampleService.getValue'中缓存key为#user.userId缓存数据。 exampleCache字段会作为一个缓存实例对象,通过@CreateCache注解可以看到,会将该字段作为cacheName为'JetCacheExampleService.getValue'缓存实例对象,本地缓存的数量最多为50,缓存类型为LOCAL,@CachePenetrationProtect注解表示该缓存实例开启保护模式。 我的业务场景是使用上述的getValue方法创建缓存实例即可。 注意: @Cached注解不能和@CacheUpdate或者@CacheInvalidate同时使用@CacheInvalidate可以多个同时使用 另外通过@CreateCache注解创建缓存实例也可以这样初始化: 更加详细的使用方法请参考JetCache官方地址。 三、源码解析 参考本人Git仓库中的JetCache项目,已做详细的注释。 简单概括:利用Spring AOP功能,在调用需要缓存的方法前,通过解析注解获取缓存配置,根据这些配置创建不同的实例对象,进行缓存等操作。 JetCache分为两部分,一部分是Cache API以及实现,另一部分是注解支持。 项目的各个子模块 jetcache-anno-api:定义JetCache注解和常量。jetcache-core:核心API,Cache接口的实现,提供各种缓存实例的操作,不依赖于Spring。jetcache-autoconfigure:完成初始化,解析application.yml配置文件中的相关配置,以提供不同缓存实例的CacheBuilder构造器jetcache-anno:基于Spring提供@Cached和@CreateCache注解支持,初始化Spring AOP以及JetCache注解等配置。jetcache-redis:使用Jedis提供Redis支持。jetcache-redis-lettuce:使用Lettuce提供Redis支持,实现了JetCache异步访问缓存的的接口。jetcache-redis-springdata:使用Spring Data提供Redis支持。jetcache-starter-redis:提供pom文件,Spring Boot方式的Starter,基于Jedis。jetcache-starter-redis-lettuce:提供pom文件,Spring Boot方式的Starter,基于Lettuce。jetcache-starter-redis-springdata:提供pom文件,Spring Boot方式的Starter,基于Spring Data。jetcache-test:提供相关测试。 常用注解与变量 在jetcache-anno-api模块中定义了需要用的缓存注解与常量,在上述已经详细的讲述过,其中@CacheInvalidateContainer注解定义value为@CacheInvalidate数组,然后通过jdk8新增的@Repeatable注解,在@CacheInvalidate注解上面添加@Repeatable(CacheInvalidateContainer.class),即可支持同一个地方可以使用多个@CacheInvalidate注解。 缓存API 主要查看jetcache-core子模块,提供各种Cache缓存,以支持不同的缓存类型 Cache接口的子关系,结构如下图: 主要对象描述: Cache:缓存接口,定义基本方法AbstractCache:抽象类,缓存接口的继承者,提供基本实现,具体实现交由不同的子类LinkedHashMapCache:基于LinkedHashMap设计的简易内存缓存CaffeineCache:基于Caffeine工具设计的内存缓存RedisCache:Redis实现,使用Jedis客户端RedisLettuceCache:Redis实现,使用Lettuce客户端MultiLevelCache:两级缓存,用于封装EmbeddedCache(本地缓存)和ExternalCache(远程缓存)RefreshCache:基于装饰器模式Decorator,提供自动刷新功能LazyInitCache:用于@CreateCache注解创建的缓存实例,依赖于Spring Cache接口 com.alicp.jetcache.Cache接口,定义了缓存实例的操作方法(部分有默认实现),以及获取分布式锁(非严格,用于刷新远程缓存)的实现,因为继承了java.io.Closeable接口,所以也提供了close方法的默认实现,空方法,交由不同缓存实例的实现去实现该方法用于释放资源,在com.alicp.jetcache.anno.support.ConfigProvider.doShutdown()方法中会调用每个缓存实例对象的close方法进行资源释放。主要代码如下: com.alicp.jetcache.Cache定义的方法大都是关于缓存的获取、删除和存放操作 其中大写的方法返回JetCache自定义的CacheResult(完整的返回值,可以清晰的知道执行结果,例如get返回null的时候,无法断定是对应的key不存在,还是访问缓存发生了异常) 小写的方法默认实现就是调用大写的方法 computeIfAbsent方法最为核心,交由子类去实现 tryLockAndRun方法会非堵塞的尝试获取一把AutoReleaseLock分布式锁(非严格),获取过程: 尝试往Redis中设置(已存在无法设置)一个键值对,key为缓存key_#RL#,value为UUID,并设置这个键值对的过期时间为60秒(默认)如果获取到锁后进行加载任务,也就是重新加载方法并更新远程缓存该锁实现了java.lang.AutoCloseable接口,使用try-with-resource方式,在执行完加载任务后会自动释放资源,也就是调用close方法将获取锁过程中设置的键值对从Redis中删除在RefreshCache中会调用该方法,因为如果存在远程缓存需要刷新则需要采用分布式锁的方式 AbstractCache抽象类 com.alicp.jetcache.AbstractCache抽象类,实现了Cache接口,主要代码如下: com.alicp.jetcache.AbstractCache实现了Cache接口的大写方法,内部调用自己定义的抽象方法(以DO_开头,交由不同的子类实现),操作缓存后发送相应的事件CacheEvent,也就是调用自己定义的notify方法,遍历每个CacheMonitor对该事件进行后置操作,用于统计信息。 computeIfAbsentImpl方法实现了Cache接口的核心方法,从缓存实例中根据缓存key获取缓存value,逻辑如下: 获取cache的targetCache,因为我们通过@CreateCache注解创建的缓存实例将生成LazyInitCache对象,需要调用其getTargetCache方法才会完成缓存实例的初始化loader函数是对加载原有方法的封装,这里再进行一层封装,封装成ProxyLoader类型,目的是在加载原有方法后将发送CacheLoadEvent事件从缓存实例中获取对应的缓存value,如果缓存实例对象是RefreshCache类型(在com.alicp.jetcache.anno.support.CacheContext.buildCache方法中会将cache包装成CacheHandlerRefreshCache),则调用RefreshCache.addOrUpdateRefreshTask方法,判断是否应该为它添加一个定时的刷新任务如果缓存未命中,则执行loader函数,如果开启了保护模式,则调用自定义的synchronizedLoad方法,大致逻辑:根据缓存key从自己的loaderMap(线程安全)遍历中尝试获取(不存在则创建)LoaderLock加载锁,获取到这把加载锁才可以执行loader函数,如果已被其他线程占有则进行等待(没有设置超时时间则一直等待),通过CountDownLatch计数器实现 AbstractEmbeddedCache本地缓存 com.alicp.jetcache.embedded.AbstractEmbeddedCache抽象类继承AbstractCache抽象类,定义了本地缓存的存放缓存数据的对象为com.alicp.jetcache.embedded.InnerMap接口和一个初始化该接口的createAreaCache抽象方法,基于InnerMap接口实现以DO_开头的方法,完成缓存实例各种操作的具体实现,主要代码如下: com.alicp.jetcache.embedded.AbstractEmbeddedCache抽象类实现了操作本地缓存的相关方法 定义了缓存实例对象本地缓存的配置信息EmbeddedCacheConfig对象定义了缓存实例对象本地缓存基于内存操作缓存数据的InnerMap对象,它的初始化过程交由不同的内存缓存实例(LinkedHashMapCache和CaffeineCache) LinkedHashMapCache com.alicp.jetcache.embedded.LinkedHashMapCache基于LinkedHashMap完成缓存实例对象本地缓存基于内存操作缓存数据的InnerMap对象的初始化工作,主要代码如下: com.alicp.jetcache.embedded.LinkedHashMapCache自定义LRUMap继承LinkedHashMap并实现InnerMap接口 自定义max字段,存储元素个数的最大值,并设置初始容量为(max * 1.4f)自定义lock字段,每个缓存实例的锁,通过synchronized关键词保证线程安全,所以性能相对来说不好覆盖LinkedHashMap的removeEldestEntry方法,当元素大于最大值时移除最老的元素自定义cleanExpiredEntry方法,遍历Map,根据缓存value(被封装成的com.alicp.jetcache.CacheValueHolder对象,包含缓存数据、失效时间戳和第一次访问的时间),清理过期的元素该对象初始化时会被添加至com.alicp.jetcache.embedded.Cleaner清理器中,Cleaner会周期性(每隔60秒)遍历LinkedHashMapCache缓存实例,调用其cleanExpiredEntry方法 Cleaner清理器 com.alicp.jetcache.embedded.Cleaner用于清理缓存类型为LinkedHashMapCache的缓存数据,请查看相应注释,代码如下: CaffeineCache com.alicp.jetcache.embedded.CaffeineCache基于Caffeine完成缓存实例对象本地缓存基于内存操作缓存数据的InnerMap对象的初始化工作,主要代码如下: com.alicp.jetcache.embedded.CaffeineCache通过Caffeine构建一个com.github.benmanes.caffeine.cache.Cache缓存对象,然后实现InnerMap接口,调用这个缓存对象的相关方法 构建时设置每个元素的过期时间,也就是根据每个元素(com.alicp.jetcache.CacheValueHolder)的失效时间戳来设置,底层如何实现的可以参考Caffeine官方地址调用com.github.benmanes.caffeine.cache.Cache的put方法我有遇到过'unable to create native thread'内存溢出的问题,所以请结合实际业务场景合理的设置缓存相关配置 AbstractExternalCache远程缓存 com.alicp.jetcache.embedded.AbstractExternalCache抽象类继承AbstractCache抽象类,定义了缓存实例对象远程缓存的配置信息ExternalCacheConfig对象,提供了将缓存key转换成字节数组的方法,代码比较简单。 RedisCache com.alicp.jetcache.redis.RedisCache使用Jedis连接Redis,对远程的缓存数据进行操作,代码没有很复杂,可查看我的注释 定义了com.alicp.jetcache.redis.RedisCacheConfig配置对象,包含Redis连接池的相关信息实现了以DO_开头的方法,也就是通过Jedis操作缓存数据 RedisLettuceCache com.alicp.jetcache.redis.lettuce.RedisLettuceCache使用Lettuce连接Redis,对远程的缓存数据进行操作,代码没有很复杂,可查看我的注释 定义了com.alicp.jetcache.redis.lettuce.RedisLettuceCacheConfig配置对象,包含Redis客户端、与Redis建立的安全连接等信息,因为底层是基于Netty实现的,所以无需配置线程池使用com.alicp.jetcache.redis.lettuce.LettuceConnectionManager自定义管理器将与Redis连接的相关信息封装成LettuceObjects对象,并管理RedisClient与LettuceObjects对应关系相比Jedis更加安全高效对Lettuce不了解的可以参考我写的测试类com.alicp.jetcache.test.external.LettuceTest MultiLevelCache两级缓存 当你设置了缓存类型为BOTH两级缓存,那么创建的实例对象会被封装成com.alicp.jetcache.MultiLevelCache对象 定义了caches字段类型为Cache[],用于保存AbstractEmbeddedCache本地缓存实例和AbstractExternalCache远程缓存实例,本地缓存存放于远程缓存前面实现了do_GET方法,遍历caches数组,也就是先从本地缓存获取,如果获取缓存不成功则从远程缓存获取,成功获取到缓存后会调用checkResultAndFillUpperCache方法从checkResultAndFillUpperCache方法的逻辑可以看到,将获取到的缓存数据更新至更底层的缓存中,也就是说如果缓存数据是从远程获取到的,那么进入这个方法后会将获取到的缓存数据更新到本地缓存中去,这样下次请求可以直接从本地缓存获取,避免与Redis之间的网络消耗实现了do_PUT方法,遍历caches数组,通过CompletableFuture进行异步编程,将所有的操作绑定在一条链上执行。实现的了PUT(K key, V value)方法,会先判断是否单独配置了本地缓存时间localExipre,配置了则单独为本地缓存设置过期时间,没有配置则到期时间和远程缓存的一样覆盖tryLock方法,调用caches[caches.length-1].tryLock方法,也就是只会调用最顶层远程缓存的这个方法 主要代码如下: RefreshCache com.alicp.jetcache.RefreshCache为缓存实例添加刷新任务,前面在AbstractCache抽象类中讲到了,在com.alicp.jetcache.anno.support.CacheContext.buildCache方法中会将cache包装成CacheHandlerRefreshCache,所以说每个缓存实例都会调用一下addOrUpdateRefreshTask方法,代码如下: 如果缓存实例配置了刷新策略并且刷新间隔大于0,则会从taskMap(线程安全)中尝试获取对应的刷新任务RefreshTask,如果不存在则创建一个任务放入线程池周期性的执行 com.alicp.jetcache.RefreshCache.RefreshTask代码如下: 刷新逻辑: 判断是否需要停止刷新了,需要的话调用其future的cancel方法取消执行,并从taskMap中删除获取缓存实例对象,如果是多层则返回顶层,也就是远程缓存实例对象如果是本地缓存,则调用load方法,也就是执行loader函数加载原有方法,将获取到的数据更新至缓存实例中(如果是多级缓存,则每级缓存都会更新)如果是远程缓存对象,则调用externalLoad方法,刷新后会往Redis中存放一个键值对,key为key_#TS#,value为上一次刷新时间 先从Redis中获取上一次刷新时间的键值对,根据上一次刷新的时间判断是否大于刷新间隔,大于(或者没有上一次刷新时间)表示需要重新加载数据,否则不需要重新加载数据 如果不需要重新加载数据,但是又是多级缓存,则获取远程缓存数据更新至本地缓存,保证两级缓存的一致性 如果需要重新加载数据,则调用tryLockAndRun方法,尝试获取分布式锁,执行刷新任务(调用load方法,并往Redis中重新设置上一次的刷新时间),如果没有获取到分布式锁,则创建一个延迟任务(1/5刷新间隔后)将最顶层的缓存数据更新至每一层 解析配置 主要查看jetcache-autoconfigure子模块,解析application.yml中jetcache相关配置,初始化不同缓存类型的CacheBuilder构造器,用于生产缓存实例,也初始化以下对象: com.alicp.jetcache.anno.support.ConfigProvider:缓存管理器,注入了全局配置GlobalCacheConfig、缓存实例管理器SimpleCacheManager、缓存上下文CacheContext等大量信息 com.alicp.jetcache.autoconfigure.AutoConfigureBeans:存储CacheBuilder构造器以及Redis的相关信息 com.alicp.jetcache.anno.support.GlobalCacheConfig:全局配置类,保存了一些全局信息 初始化构造器 通过@Conditional注解将需要使用到的缓存类型对应的构造器初始化类注入到Spring容器并执行初始化过程,也就是创建CacheBuilder构造器 初始化构造器类的类型结构如下图所示: 主要对象描述: AbstractCacheAutoInit:抽象类,实现Spring的InitializingBean接口,注入至Spring容器时完成初始化 EmbeddedCacheAutoInit:抽象类,继承AbstractCacheAutoInit,解析本地缓存独有的配置 LinkedHashMapAutoConfiguration:初始化LinkedHashMapCacheBuilder构造器 CaffeineAutoConfiguration:初始化CaffeineCacheBuilder构造器 ExternalCacheAutoInit:抽象类,继承AbstractCacheAutoInit,解析远程缓存独有的配置 RedisAutoInit:初始化RedisCacheBuilder构造器 RedisLettuceAutoInit:初始化RedisLettuceCacheBuilder构造器 AbstractCacheAutoInit com.alicp.jetcache.autoconfigure.AbstractCacheAutoInit抽象类主要实现了Spring的InitializingBean接口,在注入Spring容器时,Spring会调用其afterPropertiesSet方法,完成本地缓存类型和远程缓存类型CacheBuilder构造器的初始化,主要代码如下: 1.在afterPropertiesSet()方法中可以看到会调用process方法分别初始化本地缓存和远程缓存的构造器 2.定义的process方法: 首先会从当前环境中解析出JetCache的相关配置到ConfigTree对象中然后遍历缓存区域,获取对应的缓存类型type,进行不同类型的缓存实例CacheBuilder构造器初始化过程不同CacheBuilder构造器的初始化方法initCache交由子类实现获取到CacheBuilder构造器后会将其放入AutoConfigureBeans对象中去 3.另外也定义了parseGeneralConfig方法解析本地缓存和远程缓存都有的配置至CacheBuilder构造器中 EmbeddedCacheAutoInit com.alicp.jetcache.autoconfigure.EmbeddedCacheAutoInit抽象类继承了AbstractCacheAutoInit,主要是覆盖父类的parseGeneralConfig,解析本地缓存单有的配置limit,代码如下: LinkedHashMapAutoConfiguration com.alicp.jetcache.autoconfigure.LinkedHashMapAutoConfiguration继承了EmbeddedCacheAutoInit,实现了initCache方法,先通过LinkedHashMapCacheBuilder创建一个默认实现类,然后解析相关配置至构造器中完成初始化,代码如下: 这里我们注意到@Conditional注解,这个注解的作用是:满足SpringBootCondition条件这个Bean才会被Spring容器管理他的条件是LinkedHashMapCondition,继承了JetCacheCondition,也就是说配置文件中配置了缓存类型为linkedhashmap时这个类才会被Spring容器管理,才会完成LinkedHashMapCacheBuilder构造器的初始化JetCacheCondition逻辑并不复杂,可自行查看 CaffeineAutoConfiguration com.alicp.jetcache.autoconfigure.CaffeineAutoConfiguration继承了EmbeddedCacheAutoInit,实现了initCache方法,先通过CaffeineCacheBuilder创建一个默认实现类,然后解析相关配置至构造器中完成初始化,代码如下: 同样使用了@Conditional注解,这个注解的作用是:满足SpringBootCondition条件这个Bean才会被Spring容器管理他的条件是CaffeineCondition,继承了JetCacheCondition,也就是说配置文件中配置了缓存类型为caffeine时这个类才会被Spring容器管理,才会完成LinkedHashMapCacheBuilder构造器的初始化 ExternalCacheAutoInit com.alicp.jetcache.autoconfigure.ExternalCacheAutoInit抽象类继承了AbstractCacheAutoInit,主要是覆盖父类的parseGeneralConfig,解析远程缓存单有的配置keyPrefix、valueEncoder和valueDecoder,代码如下: RedisAutoInit com.alicp.jetcache.autoconfigure.RedisAutoInit继承了ExternalCacheAutoInit,实现initCache方法,完成了通过Jedis连接Redis的初始化操作,主要代码如下: com.alicp.jetcache.autoconfigure.RedisAutoInit是com.alicp.jetcache.autoconfigure.RedisAutoConfiguration内部的静态类,在RedisAutoConfiguration内通过redisAutoInit()方法定义RedisAutoInit作为Spring Bean 同样RedisAutoConfiguration使用了@Conditional注解,满足SpringBootCondition条件这个Bean才会被Spring容器管理,内部的RedisAutoInit也不会被管理,也就是说配置文件中配置了缓存类型为redis时RedisLettuceAutoInit才会被Spring容器管理,才会完成RedisLettuceCacheBuilder构造器的初始化 实现了initCache方法 先解析Redis的相关配置通过Jedis创建Redis连接池通过RedisCacheBuilder创建一个默认实现类解析相关配置至构造器中完成初始化将Redis连接保存至AutoConfigureBeans中 RedisLettuceAutoInit com.alicp.jetcache.autoconfigure.RedisLettuceAutoInit继承了ExternalCacheAutoInit,实现initCache方法,完成了通过Lettuce连接Redis的初始化操作,主要代码如下: 1.com.alicp.jetcache.autoconfigure.RedisLettuceAutoInit是com.alicp.jetcache.autoconfigure.RedisLettuceAutoConfiguration内部的静态类,在RedisLettuceAutoConfiguration内通过redisLettuceAutoInit()方法定义RedisLettuceAutoInit作为Spring Bean 2.同样RedisLettuceAutoConfiguration使用了@Conditional注解,满足SpringBootCondition条件这个Bean才会被Spring容器管理,内部的RedisLettuceAutoInit也不会被管理,也就是说配置文件中配置了缓存类型为redis.lettuce时RedisLettuceAutoInit才会被Spring容器管理,才会完成RedisLettuceCacheBuilder构造器的初始化 3.实现了initCache方法 先解析Redis的相关配置通过Lettuce创建Redis客户端和与Redis的连接通过RedisLettuceCacheBuilder创建一个默认实现类解析相关配置至构造器中完成初始化获取LettuceConnectionManager管理器,将通过Lettuce创建Redis客户端和与Redis的连接保存将Redis客户端、与Redis的连接、同步命令、异步命令和反应式命令相关保存至AutoConfigureBeans中 JetCacheAutoConfiguration自动配置 上面的初始化构造器的类需要被Spring容器管理,就需被扫描到,我们一般会设置扫描路径,但是别人引入JetCache肯定是作为其他包不能够被扫描到的,这些Bean也就不会被Spring管理,这里我们查看jetcache-autoconfigure模块下src/main/resources/META-INF/spring.factories文件,内容如下: org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.alicp.jetcache.autoconfigure.JetCacheAutoConfiguration 这应该是一种SPI机制,这样这个项目以外的JetCache包里面的com.alicp.jetcache.autoconfigure.JetCacheAutoConfiguration就会被Spring容器扫描到,我们来看看他的代码: 可以看到通过@Import注解,初始化构造器的那些类会被加入到Spring容器,加上@Condotional注解,只有我们配置过的缓存类型的构造器才会被加入,然后保存至AutoConfigureBeans对象中注意到这里我们注入的是SpringConfigProvider对象,加上@ConditionalOnMissingBean注解,无法再次注册该对象至Spring容器,相比ConfigProvider对象,它的区别是设置了EncoderParser为DefaultSpringEncoderParser,设置了KeyConvertorParser为DefaultSpringKeyConvertorParser,目的是支持两个解析器能够解析自定义bean在BeanDependencyManager中可以看到它是一个BeanFactoryPostProcessor,用于BeanFactory容器初始后执行操作,目的是往JetCacheAutoConfiguration的BeanDefinition的依赖中添加几个AbstractCacheAutoInit类型的beanName,保证几个CacheBuilder构造器已经初始化globalCacheConfig方法中设置全局的相关配置并添加已经初始化的CacheBuilder构造器,然后返回GlobalCacheConfig让Spring容器管理,这样一来就完成了JetCache的解析配置并初始化的功能 CacheBuilder构造器 构造器的作用就是根据配置构建一个对应类型的缓存实例 CacheBuilder的子类结构如下: 根据类名就可以知道其作用 CacheBuilder接口只定义了一个buildCache()方法,用于构建缓存实例,交由不同的实现类 AbstractCacheBuilder抽象类实现了buildCache()方法,主要代码如下: 实现了java.lang.Cloneable的clone方法,支持克隆该对象,因为每个缓存实例的配置不一定相同,这个构造器中保存的是全局的一些配置,所以需要克隆一个构造器出来为每个缓存实例设置其自己的配置而不影响这个最初始的构造器定义CacheConfig对象存放缓存配置,构建缓存实例需要根据这些配置定义的buildFunc函数用于构建缓存实例,我们在初始化构造器中可以看到,不同的构造器设置的该函数都是new一个缓存实例并传入配置信息,例如: 不同类型的构造器区别在于CacheConfig类型不同,因为远程和本地的配置是有所区别的,还有就是设置的buildFunc函数不同,因为需要构建不同的缓存实例,和上面的例子差不多,都是new一个缓存实例并传入配置信息,这里就不一一讲述了 AOP 主要查看jetcache-anno子模块,提供AOP功能 启用JetCache JetCache可以通过@EnableMethodCache和@EnableCreateCacheAnnotation注解完成AOP的初始化工作,我们在Spring Boot工程中的启动类上面添加这两个注解即可启用JetCache缓存。 @EnableMethodCache 注解的相关配置在上面的'如何使用'中已经讲过了,这里我们关注@Import注解中的CommonConfiguration和ConfigSelector两个类,将会被Spring容器管理 com.alicp.jetcache.anno.config.CommonConfiguration上面有@Configuration注解,所以会被作为一个Spring Bean,里面定义了一个Bean为ConfigMap,所以这个Bean也会被Spring容器管理,com.alicp.jetcache.anno.support.ConfigMap中保存方法与缓存注解配置信息的映射关系com.alicp.jetcache.anno.config.ConfigSelector继承了AdviceModeImportSelector,通过@Import注解他的selectImports方法会被调用,根据不同的AdviceMode导入不同的配置类,可以看到会返回一个JetCacheProxyConfiguration类名称,那么它也会被注入 com.alicp.jetcache.anno.config.JetCacheProxyConfiguration是配置AOP的配置类,代码如下: 因为JetCacheProxyConfiguration是通过@Import注解注入的并且实现了ImportAware接口,当被注入Bean的时候会先调用其setImportMetadata方法(这里好像必须添加@Configuration注解,不然无法被Spring识别出来)获取到@EnableMethodCache注解的元信息 其中定义了两个Bean: com.alicp.jetcache.anno.aop.JetCacheInterceptor:实现了aop中的MethodInterceptor方法拦截器,可用于aop拦截方法后执行相关处理 com.alicp.jetcache.anno.aop.CacheAdvisor: 1.继承了org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor,将会作为一个AOP切面 2.设置了通知advice为JetCacheInterceptor,也就是说被拦截的方法都会进入JetCacheInterceptor,JetCacheInterceptor就作为JetCache的入口了 3.根据注解设置了需要扫描的包路径以及优先级,默认是最低优先级 4.CacheAdvisor实现了org.springframework.aopPointcutAdvisor接口的getPointcut()方法,设置这个切面的切入点为com.alicp.jetcache.anno.aop.CachePointcut 5.从CachePointcut作为切入点 实现了org.springframework.aop.ClassFilter接口,用于判断哪些类需要被拦截实现了org.springframework.aop.MethodMatcher接口,用于判断哪些类中的哪些方法会被拦截在判断方法是否需要进入JetCache的JetCacheInterceptor过程中,会解析方法上面的JetCache相关缓存注解,将配置信息封装com.alicp.jetcache.anno.methodCacheInvokeConfig对象中,并把它保存至com.alicp.jetcache.anno.support.ConfigMap对象中 总结:@EnableMethodCache注解主要就是生成一个AOP切面用于拦截带有缓存注解的方法 @EnableCreateCacheAnnotation 相比@EnableMethodCache注解,没有相关属性,同样会导入CommonConfiguration类 不同的是将导入com.alicp.jetcache.anno.field.CreateCacheAnnotationBeanPostProcessor类,它继承了org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor 作为一个BeanPostProcessor,用于在Spring初始化bean的时候做一些操作 从代码中可以看到他的作用是:如果这个bean内部存在添加了带有@CreateCache注解的字段(没有添加static),会将这个字段作为需要注入的对象,解析成 com.alicp.jetcache.anno.field.LazyInitCache缓存实例 LazyInitCache的主要代码如下: 1.可以看到通过@CreateCache创建的缓存实例也可以添加@CacheRefresh和@CachePenetrationProtect注解 2.在AbstractCache抽象类的computeIfAbsentImpl方法中我们有讲到,如果缓存实例是ProxyCache类型,则会先调用其getTargetCache()方法获取缓存实例对象,所以LazyInitCache在第一次访问的时候才进行初始化,并根据缓存注解配置信息创建(存在则直接获取)一个缓存实例 总结:@EnableCreateCacheAnnotation注解主要是支持@CreateCache能够创建缓存实例 通过@EnableMethodCache和@EnableCreateCacheAnnotation两个注解,加上前面的解析配置过程,已经完成的JetCache的解析与初始化过程,那么接下来我们来看看JetCache如何处理被拦截的方法。 拦截器 从com.alicp.jetcache.anno.aop.CachePointcut切入点判断方法是否需要拦截的逻辑: 1.方法所在的类对象是否匹配,除去以"java"、"org.springframework"开头和包含"$$EnhancerBySpringCGLIB$$"、"$$FastClassBySpringCGLIB$$"的类,该类是否在我们通过@EnableMethodCache注解配置的basePackages中 2.从ConfigMap获取方法对应的CacheInvokeConfig对象,也就是获取缓存配置信息 如果是一个空对象,那么不需要被拦截,因为前面已经判断了所在的类是否需要被拦截,而这个类中并不是所有的方法都会添加缓存注解,所以这一类的方法会设置一个空对象(定义在CacheInvokeConfig内部的一个静态对象添加了final修饰),保存在ConfigMap中如果不为null,则需被拦截通过CacheConfigUtil解析这个方法的缓存注解,如果有@Cached注解或者@CacheInvalidate注解或者@CacheUpdate注解,先解析注解生成CacheInvokeConfig对象保存至ConfigMap中,然后该方法会被拦截,否在保存一个空对象不会被拦截 ConfigProvider com.alicp.jetcache.anno.support.ConfigProvide是一个配置提供者对象,包含了JetCache的全局配置、缓存实例管理器、缓存value转换器、缓存key转换器、上下文和监控指标相关信息,主要代码如下: 继承了com.alicp.jetcache.anno.support.AbstractLifecycle,查看其代码可以看到有两个方法,分别为init()初始化方法和shutdown()销毁方法,因为分别添加了@PostConstruct注解和@PreDestroy注解,所以在Spring初始化时会调用init(),在Spring容器销毁时会调用shutdown()方法,内部分别调用doInit()和doShutdown(),这两个方法交由子类实现 在doInit()方法中先启动缓存指标监控器,用于周期性打印各项缓存指标,然后初始化CacheContext缓存上下文,SpringConfigProvider返回的是SpringConfigContext 在doShutdown()方法中关闭缓存指标监控器,清除缓存实例 CacheContext com.alicp.jetcache.anno.support.CacheContext缓存上下文主要为每一个被拦截的请求创建缓存上下文,构建对应的缓存实例,主要代码如下: createCacheInvokeContext方法返回一个本次调用的上下文CacheInvokeContext,为这个上下文设置缓存函数,用于获取或者构建缓存实例,这个函数在CacheHandler中会被调用,我们来看看这个函数的处理逻辑:有两个入参,分别为本次调用的上下文和缓存注解的配置信息 首先从缓存注解的配置信息中获取缓存实例,如果不为null则直接返回,否则调用createCacheByCachedConfig方法,根据配置通过CacheBuilder构造器创建一个缓存实例对象 createCacheByCachedConfig方法: 1.如果没有定义缓存实例名称(@Cached注解中的name配置),则生成类名+方法名+(参数类型)作为缓存实例名称 2.然后调用__createOrGetCache方法 __createOrGetCache方法: 1.通过缓存实例管理器SimpleCacheManager根据缓存区域area和缓存实例名称cacheName获取缓存实例对象,如果不为null则直接返回,判断缓存实例对象是否为null为进行两次确认,第二次会给当前CacheContext加锁进行判断,避免线程不安全 2.缓存实例对象还是为null的话,先判断缓存区域area是否添加至缓存实例名称中,是的话"area_cacheName"为缓存实例名称,然后调用buildCache方法创建一个缓存实例对象 buildCache方法:根据缓存实例类型构建不同的缓存实例对象,处理逻辑如下: CacheType为LOCAL则调用buildLocal方法: 1.1. 从GlobalCacheConfig全局配置的localCacheBuilders(保存本地缓存CacheBuilder构造器的集合)中的获取本地缓存该缓存区域的构造器,在之前讲到的'JetCacheAutoConfiguration自动配置'中有说到过,会将初始化好的构造器从AutoConfigureBeans中添加至GlobalCacheConfig中  1.2. 克隆一个 CacheBuilder 构造器,因为不同缓存实例有不同的配置  1.3. 将缓存注解的配置信息设置到构造器中,有以下配置:      - 如果配置了localLimit,则设置本地缓存最大数量limit的值      - 如果CacheType为BOTH并且配置了localExpire(大于0),则设置有效时间expireAfterWrite的值为localExpire,否则如果配置的expire大于0,则设置其值为expire      - 如果配置了keyConvertor,则根据该值生成一个转换函数,没有配置的话在初始化构造器的时候根据全局配置可能已经生成了一个转换函数(我一般在全局配置中设置)      - 设置是否缓存null值  1.4. 通过调用构造器的buildCache()方法构建一个缓存实例对象,该方法在之前讲到的'CacheBuilder构造器'中有分析过 CacheType为REMOTE则调用buildRemote方法:   1.1. 从GlobalCacheConfig全局配置的remoteCacheBuilders(保存远程缓存CacheBuilder构造器的集合)中的获取远程缓存该缓存区域的构造器  1.2. 克隆一个 CacheBuilder 构造器,因为不同缓存实例有不同的配置  1.3. 将缓存注解的配置信息设置到构造器中,有以下配置:      - 如果配置了expire,则设置远程缓存有效时间expireAfterWrite的值      - 如果全局设置远程缓存的缓存key的前缀keyPrefix,则设置缓存key的前缀为"keyPrefix+cacheName",否则我为"cacheName"      - 如果配置了keyConvertor,则根据该值生成一个转换函数,没有配置的话在初始化构造器的时候根据全局配置可能已经生成了一个转换函数(我一般在全局配置中设置)      - 如果设置了serialPolicy,则根据该值生成编码和解码函数,没有配置的话在初始化构造器的时候根据全局配置可能已经生成了编码函数和解码函数(我一般在全局配置中设置)      - 设置是否缓存null值   1.4. 通过调用构造器的buildCache()方法构建一个缓存实例对象 CacheType为BOTH则调用buildLocal方法构建本地缓存实例,调用buildRemote方法构建远程缓存实例:    1.1. 创建一个MultiLevelCacheBuilder构造器    1.2. 设置有效时间为远程缓存的有效时间、添加local和remote缓存实例、设置是否单独配置了本地缓存的失效时间(是否有配置localExpire)、设置是否缓存null值    1.3. 通过调用构造器的buildCache()方法构建一个缓存实例对象 2.设置刷新策略RefreshPolicy,没有的话为null 3.将缓存实例对象封装成CacheHandlerRefreshCache对象,用于后续的添加刷新任务,在之前的'AbstractCache抽象类'有讲到 4.设置是否开启缓存未命中时加载方法的保护模式,全局默认为false 5.将缓存实例添加至监控管理器中 JetCacheInterceptor 被拦截后的处理在com.alicp.jetcache.anno.aop.JetCacheInterceptor中,代码如下: 从ConfigMap中获取被拦截的方法对象的缓存配置信息,如果没有则直接执行该方法,否则继续往下执行 根据CacheContext对象(SpringCacheContext,因为在之前讲到的'JetCacheAutoConfiguration自动配置'中有说到注入的是SpringConfigProvider对象,在其初始化方法中调用newContext()方法生成SpringCacheContext)调用其createCacheInvokeContext方法为本次调用创建一个上下文CacheInvokeContext,并设置获取缓存实例函数,具体实现逻辑查看上面讲到的CacheContext 设置本次调用上下文的targetObject为被拦截对象,invoker为被拦截对象的调用器,method为被拦截方法,args为方法入参,cacheInvokeConfig为缓存配置信息,hiddenPackages为缓存实例名称需要截断的包名 通过CacheHandler的invoke方法继续往下执行 CacheHandler com.alicp.jetcache.anno.method.CacheHandler用于JetCache处理被拦截的方法,部分代码如下: 直接查看invokeWithCached方法: 获取缓存注解信息根据本地调用的上下文CacheInvokeContext获取缓存实例对象(调用其cacheFunction函数),在CacheContext中有讲到如果缓存实例不存在则直接调用invokeOrigin方法,执行被拦截的对象的调用器根据本次调用的上下文CacheInvokeContext生成缓存key,根据配置的缓存key的SpEL表达式生成,如果没有配置则返回入参对象,如果没有对象则返回"_ $JETCACHE_NULL_KEY$_"根据配置condition表达式判断是否需要走缓存创建一个CacheLoader对象,用于执行被拦截的对象的调用器,也就是加载原有方法调用缓存实例的computeIfAbsent(key, loader)方法获取结果,这个方法的处理过程可查看'缓存API'这一小节 到此这篇关于JetCache 缓存框架的使用以及源码分析的文章就介绍到这了,更多相关JetCache 缓存框架内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

上一篇:没有了
下一篇:RPMI-8226 人多发性骨髓瘤细胞系价格 1236元/cells 厂家:上海宾穗生物科技有限公司