Spring Cache

Spring Cache

Spring Cache

  • @Cacheable
  • @CachePut
  • @CacheEvict
  • @Caching

先上一段代码,按照例子讲解。

@Repository("userRepositoryJdbc")
public class UserRepositoryJdbc implements UserRepository {

    public static final String CACHE_KEY_PREFIX_USERNAME = "username_";
    public static final String CACHE_KEY_PREFIX_MOBILE = "mobile_";
    public static final String CACHE_KEY_PREFIX_EMAIL = "email_";

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    @Caching(evict = {
            @CacheEvict(value = USER_CACHE, key = "#root.targetClass.CACHE_KEY_PREFIX_MOBILE + #user.phone"),
            @CacheEvict(value = USER_CACHE, key = "#root.targetClass.CACHE_KEY_PREFIX_USERNAME + #user.username")
    })
    public void updateUser(final User user) {
        final String sql = " update user_ set username = ?, password = ?, phone = ?,email = ? where guid = ? ";
        this.jdbcTemplate.update(sql, ps -> {
            ps.setString(1, user.username());
            ps.setString(2, user.password());

            ps.setString(3, user.phone());
            ps.setString(4, user.email());

            ps.setString(5, user.guid());
        });
    }

    @Override
    @Cacheable(value = USER_CACHE, key = "#root.targetClass.CACHE_KEY_PREFIX_USERNAME + #username")
    public User findByUsername(String username) {
        final String sql = " select * from user_ where username = ? and archived = 0 ";
        final List<User> list = this.jdbcTemplate.query(sql, new Object[]{username}, userRowMapper);

        User user = null;
        if (!list.isEmpty()) {
            user = list.get(0);
            user.privileges().addAll(findPrivileges(user.id()));
        }

        return user;
    }


    @Override
    @Cacheable(value = USER_CACHE, key = "#root.targetClass.CACHE_KEY_PREFIX_MOBILE + #mobile")
    public User findByMobile(String mobile) {
        final String sql = " select * from user_ where phone = ? and archived = 0 ";
        final List<User> list = this.jdbcTemplate.query(sql, new Object[]{mobile}, userRowMapper);

        User user = null;
        if (!list.isEmpty()) {
            user = list.get(0);
//            user.privileges().addAll(findPrivileges(user.id()));
        }
        return user;
    }

官网

问题与解决方案

我遇到问题了才来看的。问题是在这样。
问题之前的代码

@Cacheable(cacheNames="userCache", key="#username")
public User findUserByUsername(String username)

@Cacheable(cacheNames="userCache", key="#mobile")
public User findUserByMobile(String mobile)

@CacheEvict(cacheNames="userCache", key="#user.username")
public User updateUser(User user)  

用户通过手机号登陆,修改密码成功后,同样的数据再次提交,本应显示原密码不正确,但是没有显示,反而又是密码修改成功,该删除的缓存没有删除掉。需要一次删除多个Entry(后文实现了)。
还有,我想能不能,findUserByMobile执行之后,findUserByUsername就可以直接从缓存调取,需要一次存多个缓存Entry,(后文没有实现)。

Spring Cache原理

Spring Cache注解作用于方法,基于Proxy或AspcetJ实现,在被注解的方法运行时,Spring Cache会查看这个方法有没有被执行过,如果有,则直接返回以前的执行结果,该方法并未真正执行,如果没有,该方法就会真正执行,并按照规则将结果放入缓存,以备以后直接调用。(我知道,上一句话是不精确的,官网上的大概意思是这样,这样更加容易理解作用于方法的Cache注解,Cache机制)。
Spring Cache具体是如何知道当前方法配带当前参数有没有“被执行”过呢?SpringCache通过cache的name和key来确定缓存中有没有想要的Entry,来决定该方法还需不需要真的执行。真正是”@Cacheable“来判断方法需不需要真正执行。

@Cacheable

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}

默认值是name,允许多个name,这里需要注意一下,准确的说“并不是允许多个name",一个name对应一个cache实例,一个cache中有多个条目(Entry),每个Entry就是个Key,Value。

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)  

key的默认机制,默认生成器,自定义生成器,不多说,看官网。

这里需要说一下,多个name(多个cache)的情况,只要有一个cache中包含该key,方法就不会执行,而且所有其他cache中会更新这个entry。

详细的cache高级用法,包括自定义Cache解决方案,多线程,按条件缓存,看官网。

我试验过caching-with-multiple-keys,用#result?.mobile做第二个key,结果不成功。

失败案例

@Override
@Cacheable(key="{#bar.name, #bar.id}")
public int foo(Bar bar) {  
    ....
}

调试方法
how-to-iterate-on-a-cache-entries

import net.sf.ehcache.Cache;  
import org.springframework.cache.ehcache.EhCacheCache;  
import org.springframework.cache.ehcache.EhCacheCacheManager;  
...
...
    @Autowired
    @Qualifier("profileCacheManager")
    EhCacheCacheManager ehCacheCacheManager;

    @RequestMapping("/test_cache_list")
    public Object testCacheManager() {

        EhCacheCache ehCacheCache = (EhCacheCache) ehCacheCacheManager.getCache("userCache");
        Cache cache = (Cache) ehCacheCache.getNativeCache();
        return cache.getKeys();
    }
...

失败输出结果

[mobile_18612345678,[username_18612345678,mobile_null]]

mobile_null这样子的key,在方法执行前,就已经生成,即使我换成,
@Caching(cacheable={@Cacheable(key=""),@Cacheable(key="")})还是解决不了,就这样吧。

注解SpEL中可用变量

cache_spel_variable

@CachePut

看见@CachePut的时候心中一喜,但是最终还是解决一次添加多个Entry无望。 官方做了重要说明 @CachePut和@Cacheable非常不鼓励一起用,@Cacheable会根据缓存情况判断目标方法需不需要真正执行,而@CachePut是强制目标方法必须执行,然后更新缓存指定条目。

@CacheEvict

删除指定缓存,beforeInvocation属性默认为false,意思是目标方法成功执行完后,再更新缓存,如果目标方法执行出异常,则不会更新缓存。

@Caching

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)  

一次删除多条实现。

CacheManager

    <cache:annotation-driven cache-manager="profileCacheManager"/>

    <!-- cache manager -->
    <bean id="profileCacheManager" name="profileCacheManager"
          class="org.springframework.cache.ehcache.EhCacheCacheManager"
          p:cacheManager-ref="profileEhcache"/>

    <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" name="profileEhcache"
          id="profileEhcache" 
          p:configLocation="classpath:profile-ehcache.xml" p:cacheManagerName="profileEhcache"/>

多个CacheManager实例的情况。为什么会遇到这个问题,我想做一个后台和RestApi的脚手架项目,Admin Moudle和Api Moudle分开,共同依赖 Common Moudle,Profile Moudle,Business Moudle,另外Api自己还依赖一个OAuth Moudle,用于接口OAuth2.0认证。Admin Api 分别打war包,分成两个应用部署。问题出现在测试Api的时候,Profile Moudle 是会员数据包括会员头像电话号码地址等等,自己有一个CacheManager来管理自己缓存,另外OAuth Moudle也有自己的CacheManager来管理refreshToken,accessToken之类的跟认证相关的缓存,集成的时候,运行调用getUserByUsername出现了异常Cannot find cache named for userCache...,调试了半天才发现EhCacheManager的实例化机制,EhCacheManagerFactoryBean有个shared属性,默认是false。就是如果你不想要冗余的EhCacheManager实例,只要一个,就把这个shared设置为true,第一次实例化bean以后,再写,全部是跟第一个为同一个实例,所以oauthCacheManager实例化之后,profileEhcache并没有真正初始化,我之前把shared属性设置为了true。默认的情况下是false,也就是允许多个EhCacheManager实例,但是要指定cacheManagerName,好了,最后说明一下这里的CacheManager说的是EhCacheManager。

be-careful-with-cache-managers

好了,就这样吧。

References
how-to-iterate-on-a-cache-entries
caching-with-multiple-keys 未实验成功
cacheable-key-on-multiple-method-arguments 未实验成功
be-careful-with-cache-managers

Related Article