专栏首页Java识堂用注解解决业务逻辑和缓存逻辑的深度耦合

用注解解决业务逻辑和缓存逻辑的深度耦合

介绍

spring3.1引入了基于注解的缓存技术,即spring cache模块,它不是一个具体的缓存实现方案,而是一个对缓存使用的抽象。你可以类比为JDBC,定义了一系列缓存操作的接口,由具体的缓存来实现,如Ehcache,Redis等。

演示一下我们一般是怎么操作缓存的

实体类

@Data
@NoArgsConstructor
public class Account {
  private int id;
  private String name;
  private String password;

  public Account(String name) {
    this.name = name;
  }
}

缓存类,这个简单用ConcurrentHashMap来演示一下

public class Cache<T> {

  private Map<String, T> cache = new ConcurrentHashMap<String,T>();

  public T getValue(Object key) {
    return cache.get(key);
  }

  public void addOrUpdateCache(String key,T value) {
    cache.put(key, value);
  }

  // 根据 key 来删除缓存中的一条记录
  public void evictCache(String key) {
    if(cache.containsKey(key)) {
      cache.remove(key);
    }
  }

  // 清空缓存中的所有记录
  public void evictCache() {
    cache.clear();
  }
}

服务类

public class AccountService {

  private Cache<Account> cache = new Cache<>();

  public Account getAccountByName(String name) {
    Account result = cache.getValue(name);
    // 如果在缓存中,则直接返回缓存的结果
    if (result != null) {
      System.out.println("get from cache " + name);
      return result;
    }
    result = getFromDB(name);
    // 将数据库查询的结果更新到缓存中
    if (result != null) {
      cache.addOrUpdateCache(name, result);
    }
    return result;
  }

  private Account getFromDB(String name) {
    System.out.println("get from db " + name);
    return new Account(name);
  }
}

测试类

public class Main {

  public static void main(String[] args) {

    /**
     * get from db aaa
     * get from cache aaa
     */
    AccountService s = new AccountService();
    s.getAccountByName("aaa");
    s.getAccountByName("aaa");
  }
}

结果和我们预想的一样,第一次从数据库中拿,第二次就从缓存中拿

这样写有什么问题呢?

1.缓存代码和业务代码耦合度太高 2.目前缓存存储这块写的比较死,不能灵活的切换为第三方模块,当然你可以再抽象一层。

我们用Spring Cache改造一下上面的代码

在Spring Boot中使用Spring Chache

1.在pom文件中加入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2.在配置类中加@EnableCaching注解

@SpringBootApplication
@EnableCaching
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class, args);
    }
}

以@Enable开头的这种类型的注解在Spring Boot和Spring Cloud项目中还是经常出现的,这种类型的注解是一个开关注解,对于调试还是非常方便的。比如调试中你不想用缓存,你就可以把这个注解注释掉。这样缓存相关的注解都不能使用了。

服务类改成如下形式

@Service
public class AccountService {

  @Cacheable(value = "cache", key = "#name")
  public Account getAccountByName(String name) {
    return getFromDB(name);
  }

  @Cacheable(value = "cache1", key = "#name")
  public Account getAccountByNameCache1(String name) {
    return getFromDB(name);
  }

  @Cacheable(value = "cache2", key = "#name")
  public Account getAccountByNameCache2(String name) {
    return getFromDB(name);
  }

  private Account getFromDB(String name) {
    System.out.println("get from db " + name);
    return new Account(name);
  }
}

现在你只看getAccountByName方法

@Test
public void test0() {

    /**
     * get from db 1
     * get from db 2
     */
    accountService.getAccountByName("1");
    accountService.getAccountByName("1");
    accountService.getAccountByName("2");
    accountService.getAccountByName("2");
}

对于不同的key,第一次走数据库,第二次走缓存。完全符合我们的预期,而我们只用了一个注解。

spring cache的整体设计就类似下面这个map

Map<String, Map<String, String>> cacheManager;

最外层的map是缓存的名字(因为有可能有多个缓存),里面的map是缓存的key和缓存的value。

最外层的map对应spring cache的CacheManager接口(管理多个缓存),实现类有EhCacheCacheManager和ConcurrentMapCacheManager等

里面的map对应spring cache的Cache接口(定义缓存的具体操作,如put和get等),实现类有EhCacheCache和ConcurrentMapCache等

Spring cache默认使用的是ConcurrentMapCacheManager,即把缓存数据放在ConcurrentHashMap中。所以如果你想使用第三方缓存只要注入对应的CacheManager实现类和Cache实现类就行,或者你自己写实现类

接着来说上面用到的注解

@Cacheable(value = "cache", key = "#name")

@Cacheable的value值为cache的名字,key为缓存的key(使用SpEL表达式),缓存的key有多种指定方式,我这里只按照name进行了缓存。实际的key有多个组合方式,还能设定key的缓存条件。更多使用方式看最后的参考资料。

所以现在你明白@Cacheable的作用了把,如果能从缓存中取到值,就从缓存中取,否则就从数据库中取,如果取到值,再把值放到缓存中。背后的原理就是Spring Aop我就不深入解释了。

当初我为了验证我的想法,对getAccountByName写了好几个,就是@Cacheable中的value不同。

用来测试@Cacheable的value值是指定缓存的名字

@Test
public void test1() {
    /**
     * get from db 1
     * get from db 1
     */
    accountService.getAccountByNameCache1("1");
    accountService.getAccountByNameCache2("1");
}

打印所有缓存的名字

@Autowired
CacheManager cacheManager;

@Test
public void test3() {

    /**
     * get from db 1
     * get from db 1
     * cache2
     * cache1
     */
    accountService.getAccountByNameCache1("1");
    accountService.getAccountByNameCache2("1");
    Collection<String> collection = cacheManager.getCacheNames();
    collection.forEach(item ->{
        System.out.println(item);
    });
}

我再演示一下其他注解的使用

@Service
@CacheConfig(cacheNames = "cache")
public class AccountService1 {

  @Cacheable(key = "#name")
  public Account getAccountByName(String name) {
    return getFromDB(name);
  }

  @CachePut(key = "#account.getName()")
  public Account updateAccount(Account account) {
    return account;
  }

  @CacheEvict(key = "#name")
  public void deleteAccount(String name) {

  }

  private Account getFromDB(String name) {
    System.out.println("get from db " + name);
    return new Account(name);
  }
}

在前面的示例中,我们用到了@Cacheable注解,每次都得指明value属性,即缓存的名字。如果不想每次指定缓存的名字,就可以用@CacheConfig注解在类上统一指定一个缓存的名字。

@CachePut能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用,所以这个注解经常用在更新操作上

@CacheEvict根据条件对缓存清空,所以一般用在删除方法上

@Test
public void test6() {
    /**
     * get from db aaa
     * get from db aaa
     */
    accountService1.getAccountByName("aaa");
    accountService1.deleteAccount("aaa");
    accountService1.getAccountByName("aaa");
}

本文分享自微信公众号 - Java识堂(erlieStar),作者:李立敏

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一张千万级别数据的表想做分页,如何优化?

    当进行分页时,MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后放弃前 offset 行,返回 N 行。例如 limit 10000,...

    Java识堂
  • 聊聊登录那些事

    原来分享过一篇文章,Java自定义注解及应用,当时为了能突出重点,直接在url中传了用户的所属角色,并写了一般的做法。加上最近看了一些人的简历,发现神奇的相似,...

    Java识堂
  • 缓存雪崩,缓存穿透,缓存击穿出现的原因及解决方案?

    假设有如下一个系统,高峰期请求为5000次/秒,4000次走了缓存,只有1000次落到了数据库上,数据库每秒1000的并发是一个正常的指标,完全可以正常工作,但...

    Java识堂
  • mysql 有4种不同的索引

    主键索引(PRIMARY) 数据列不允许重复,不允许为NULL,一个表只能有一个主键 唯一索引(UNIQUE) 数据列不允许重复,允许为NULL值,一个表允许...

    用户7737280
  • java设计模式之组合模式

    组合(Composite Pattern)模式的定义:有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对...

    用户4361942
  • RabbitMq的消息队列类型direct、fanout、topic、headers(headers抛弃)

    1 服务端 server 将 消息 msg_txt 投递 到 交换器 exchange_name 路由键为 routing_key_name ,当 有队列 qu...

    93年的老男孩
  • ubuntu下mysql的安装以及基本命令

    打开终端,输入sudo apt-get install mysql-server

    bear_fish
  • python中的内置函数(双下划线) 原

    如果我们是直接执行某个.py文件的时候,该文件中那么”__name__ == '__main__'“是True,但是我们如果从另外一个.py文件通过import...

    晓歌
  • django orm(2)

    分组查询主要应用在比如查询班级中男生、女生的个数等需要先分组再查询的场景,分组操作使用的annotate内部调用的是SQL语句group by,分着查询需要和聚...

    GH
  • bug诞生记——临时变量、栈变量导致的双杀

            这是《bug诞生记》的第一篇文章。本来想起个文艺点的名字,比如《Satan(撒旦)来了》,但是最后还是想让这系列的重心放在“bug的产生过程”和...

    方亮

扫码关注云+社区

领取腾讯云代金券