@Deprecated 不建议阅读
一. Spring Cache的基本应用
Spring对cache有注解层的支持,我们可以非常方便的使用这些注解来实现业务中的缓存逻辑。建议使用Spring的同学手下一定有一个Spring源码的阅读环境。Spring主要的cache注解如下:
(源码位于spring-context -> cache -> annotation)
@interface Cacheable
这个注解应该是最常用的一个注解,这个注解可以标注一个方法,这个方法返回的数据会被缓存,下次调用将直接返回缓存中的数据。@interface CachePut
这个注解标注的方法返回的数据也会被缓存,但是每次调用该方法都将执行该方法,并将最新数据放入相关联的缓存之中。所以即使缓存中已经有数据,还是会去执行这个方法。需要体会一下和Cacheable
的区别。@interface CacheEvict
这个注解标注的方法被调用时,CacheEvict
指定的缓存数据将失效。
看一下Cacheable
的源码,这里大致解释了一下主要字段的作用,其他的注解也类似。
1 |
|
当然只有注解是不行的,我们还需要具体的cache,这里我们使用的是GuavaCache
。Spring 5 已经放弃了对GuavaCache
的支持,据说是性能原因,取而代之的是CaffeineCache
和EhCacheCache
。之后的文章会介绍并对比GuavaCache
、CaffeineCache
和EhCacheCache
的区别。
1 |
|
这里注册了两个cache,名称分别是CACHE_FIVE_MINUTE
和CACHE_ONE_HOUR
。并注册了一个KeyGenerator
,用于生产cache的key。这里的生成策略很简单,方法public Object generate(Object target, Method method, Object... params)
的参数值得注意一下,target是调用被缓存方法的对象,method是被调用的方法,params是方法的参数签名,这种参数组合在Spring中非常常见,在Java中的InvocationHandler
也使用了这种参数组合,public Object invoke(Object proxy, Method method, Object[] args)
,表示”某个对象调用了某个方法”。
KeyGenerator
简单的返回了一个独一无二的key,这里会导致一个很麻烦的问题,下面来详细讨论。
二. Cache Key 引发的问题
上面描述了Spring Cache的一个简单实践,我们已经可以很方便的在业务逻辑代码中使用我们注册的Cache了:
1 | /** |
我们设置了缓存5分钟自动失效,缓存失效后,再次调用getAllBooksName()
会直接去数据库拿数据,然后再次缓存。但是我们面临一个严峻的问题。
假设有以下情景:有一个用户在数据库中新增了一本书,然后用户需要立即使用刚刚新增的这本书籍,这是就出现了问题,因为缓存中并没有这个数据,而只有当缓存到期自动失效后,再次从数据库中加载数据时,才会加载到刚刚新增的书籍到缓存中。这时,缓存和数据库中的数据出现了不一致。而且不一致的时间最长达到5分钟。
要解决这个问题就不得不提到”大名鼎鼎”的 缓存更新策略问题,这个问题是可以算作cache的一个经典问题。这里介绍一个常用的策略,称作:Cache Aside。简单描述一下这个更新策略:
- 应用在查询数据的时候,先从缓存Cache中读取数据,如果缓存中没有,则再从数据库中读取数据,得到数据库的数据之后,将这个数据也放到缓存Cache中。
- 如果应用要更新某个数据,也是先去更新数据库中的数据,更新完成之后,则通过指令让缓存Cache中的数据失效。
我们的问题有了解决办法:那就是当这个用户在数据库新增完书籍之后(注意这里一定是确保添加完毕之后),主动让缓存失效,当用户再次调用获取所有书籍的方法时,由于缓存已经失效,方法会从数据库中去获取最新的数据,这是缓存中的数据和数据库一致了。要注意的是,这里的一致并不是强一致,因为在并发环境下,用户添加完数据和调用主动缓存失效方法之间,可能会有其他的用户读取数据,这时,缓存仍然是旧缓存(因为发生在主动使缓存失效之前),但是数据库中已经有了最新的数据(发生在添加完数据之后),所以缓存的数据和数据库中的数据有一瞬间是不一致的。但是我们了解这一点就行,因为这种应用情景下,并不需要如此严格的数据一致性。
如果需要特定的缓存失效,我们只需要新增一个方法,然后标注上CacheEvict
注解即可:
1 | /** |
这个方法不需要实现任何逻辑,当这个方法被调用时,缓存就失效了。
但是注意key这个参数,这里需要让缓存中一个特定的key对应的数据失效时,我们需要指定这个key,然而不幸的是,在上文代码中所见,使用了KeyGenerator
去自动生成一个key,当再次需要这个key时,只能去按照规则去还原这个key(根据Object target, Method method, Object... params
参数),这无疑是不能接受的。
三. Cache Key 如何管理?
这是本篇文章的重点,在缓存中,数据由CacheName和一个Key唯一决定,如果需要对特定的数据进行修改,则需要根据CacheName和Key去找到对应的数据。CacheName在本文实践中,只有两个:CACHE_FIVE_MINUTE
和 CACHE_ONE_HOUR
,所以CacheName的获得并不是问题。真正的问题是Key,因为我们已经使用了一个KeyGenerator
去自动生和被调用方法一一对应的Key,由于Key是和被调用方法一一对应的,在其他的方法中需要得到这个Key将变得困难。
1. 全局Enum
让我们忘记KeyGenerator
这个东西。回到最初,我们最原始的管理方法是什么呢?我想最简单的方法就是定义一个全局的Enum
类,里面去管理所有的CacheKey,大概像这样:
1 | public enum CacheKeyEnum { |
然后上文中的代码,缓存注解就变成了这样:
1 |
|
好了,这样通过一个全局Enum去管理所有的CacheKey的方式,比较好的解决掉了CacheKey的管理问题,目前看是这样。
缺点:如果你是一个经历过数十万行代码的人,相信你一定见过这种工程中的全局Enum
,可能还会遇到全局的public static final String = "xxx";
管理类,不得不说这是一种十分方便的管理常量的方式,但是随着工程的膨胀,这种类也将变得越来越大,你很难从一堆看着差不多的大写常量中找到自己想要的那个,这种情况在多人合作时将变得更加严重,因为你不确定别人是否已经定义了你想定义的常量,即使你在几百个常量中找到了你觉得可能是你需要的常量,你也不确定这就是你想定义的常量,你可能需要去找作者交流一下,即使你不管这些,定义一个属于你自己的常量,这可能导致同一个系统中存在多个意义一样但是命名不同的常量,极端情况下,大家可能自顾自的去添加各种常量,这部分系统将变得不可维护。
不难发现,一个系统中的CacheKey的数目绝对不是一个小数量,使用这种方式无异饮鸩止渴。
2. 抽象Cache的管理
另外一条思路就是,抽象一个AbstractCacheService,用来管理所有的Cache相关的工作。
缺点:很明显,以后我们所有需要Cache的Service,都会继承自AbstractCacheService,然而继承这种做法并不是Java工程师首先会考虑的解决问题的方式,因为继承太重了,这是由于Java中的单继承机制引起的。在决定去继承一个类之前,需要确定子类没有其他更重要的类需要继承,而且将来也不会有,因为要修改所有子类继承的父类类型是一个庞大无趣的机械工作。这使得我们在使用继承时慎之又慎,除非子类是对父类的直接扩展。一个很好的反面教材是Java标准库中的Observer
和Observable
,这是Java API对观察者模式的通用封装,而我们却鲜少见到有人用,原因和上述的原因是相同的,我们只能选择去继承Observable
,而不是实现一个接口。
未完待续…