Spring boot cache first experience

spring boot cache first experience

1. Project to build

Using MySQL as the database, spring boot integrated mybatis to operate the database, so use springboot the cache components, we need to build a simple ssm environment.

The first is the project dependent

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.48</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Database test data

CREATE TABLE `student`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gender` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, 'eric', 'male', 22);
INSERT INTO `student` VALUES (2, 'alice', 'female', 23);
INSERT INTO `student` VALUES (3, 'bob', 'male', 21);

Entity corresponding class code as follows:

public class Student {
    private Integer id;
    private String name;
    private String gender;
    private Integer age;
    //省略构造函数,getter,setter,toString
}

Corresponding mapper:

public interface StudentMapper {
    @Select("select * from student where id = #{id}")
    Student getStudentById(Integer id);
}

Corresponding service:

@Service
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    public Student getStudentById(Integer id) {
        return studentMapper.getStudentById(id);
    }
}

Corresponding test classes:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoCacheApplicationTests {
    @Autowired
    private StudentService studentService;

    /**
     * 测试mybatis是否正确配置
     */
    @Test
    public void contextLoads() {
        System.out.println(studentService.getStudentById(1));
    }

}

Run the above test methods to successfully print

student{id=1, name='eric', gender='male', age=22}

The project to build the basic rack is successful, the next step is to use the cache annotations springboot provided to test.

Prior to this, first some background.

The first is the JSR107 cache specification, Java Caching defines five core interfaces are CachingProvider, CacheManager, Cache, Entry
and Expiry.

  • CachingProvider
    defines create, configure, acquire, manage and control multiple CacheManager. An application can access multiple CachingProvider at runtime.
  • CacheManager
    defines create, configure, acquire, manage and control multiple uniquely named Cache, Cache these exist in the context of CacheManager. A CacheManager only a CachingProvider owned.
  • Cache
    is a data structure similar to the Map Key and temporarily stored as a value of the index. Cache is a only a CacheManager owned.
  • Entry
    is a key-value pairs stored in the Cache.
  • Expiry
    each stored in the Cache entry has a valid definition. Once over this time, the entry for the expired state. Once expired, the entry will not be accessible, update, and delete. Cache validity period can be set by ExpiryPolicy.

Snipaste_2019-09-23_21-58-01.png

Spring start defining org.springframework.cache.Cache and org.springframework.cache.CacheManager interfaces from 3.1 to unify the different cache technology, and support the use of JCache] [JSR-107 annotations to simplify our development.

We look at the basic structure of the Cache interface:

Snipaste_2019-09-25_23-33-25.png

Cache interface specification for the cache operation of various components of the basic cache, spring provides a variety of commonly used xxxCache implemented, for example: RedisCache, EhCacheCache, ConcurrentMapCache like.

In the current add-dependent, and can be found to achieve Cache

Snipaste_2019-09-25_23-32-20.png

Each time method requires the cache function call, Spring will be to check whether the target method specified parameters have been called, and then returned to the user after if it is available directly from the cache, there is no way the result is called a cache, after the data are acquired directly from the cache.

So use the cache to consider the following aspects:

  • Are you sure you want to cache method
  • Method for determining cache policies (such as key setting, cached data is the use of json format or Java serialization)
  • How cache and database to ensure data consistency
  • Each read previously cached data from the cache

First, not all methods require a cache, in general, are not frequently accessed and frequently modified data only needs to be cached.

key generation strategy can be used directly attribute to specify the key, you can also specify keyGenerator

Way in the case of the serial number are using the Java cache data by default, we can save it as json format, see project needs.

缓存的一致性,这个比较复杂,本文不涉及到高并发情况下缓存和数据库一致的讨论,只是保证在数据修改或删除时,及时地更新缓存中的数据。换句话说,就是数据在缓存之后,如果之后调用了修改的方法,把数据修改了,需要CachePut注解及时地把缓存里的数据也一并修改,或者,调用了删除的方法,需要使用CacheEvict注解来删除相应缓存的数据。

至于每次都从缓存中读取已经缓存过的数据,这个事情就交给Spring来自动处理吧。

Cache 缓存接口,封装缓存的基本操作
CacheManager 缓存管理器,管理各种缓存组件,一个应用程序可以有多个缓存管理器
@Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@CacheEvict 清空缓存
@CachePut 保证方法被调用,又希望结果被缓存,一般用于修改数据。
@EnableCaching 开启基于注解的缓存
keyGenerator 缓存数据时key生成策略
serialize 缓存数据时value序列化策略

@CachePut@Cacheable两个注解的区别是什么呢?

@CachePut:这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中。

@Cacheable:当重复使用相同参数调用方法的时候,方法本身不会被调用执行,即方法本身被略过了,取而代之的是方法的结果直接从缓存中找到并返回了。

​ 对于@CachePut这个注解,它的作用是什么呢,每次方法都执行,那么缓存的意义是什么呢?答案很简单,同一个缓存实例的相同的key的缓存的数据,可以用@CachePut更新,而@Cacheable在取值的时候,是@CachePut更新后的值。但同时也要注意确保是同一个缓存实例对象,并且key要保证一致!!!

@Cacheable,@CachePut,@CacheEvict注解的常用属性如下:

属性 作用 示例
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如: @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如: @Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存,在调用方法之前之后都能判断 例如: @Cacheable(value=”testcache”,condition=”#userName.length()>2”)
allEntries (@CacheEvict ) 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 例如: @CachEvict(value=”testcache”,allEntries=true)
beforeInvocation (@CacheEvict) 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 例如: @CachEvict(value=”testcache”,beforeInvocation=true)
unless (@CachePut) (@Cacheable) 用于否决缓存的,不像condition,该表达式只在方法执行之后判断,此时可以拿到返回值result进行判断。条件为true不会缓存,fasle才缓存 例如: @Cacheable(value=”testcache”,unless=”#result == null”)

Cache SpEL available metadata

名字 位置 描述 示例
methodName root object 当前被调用的方法名 #root.methodName
method root object 当前被调用的方法 #root.method.name
target root object 当前被调用的目标对象 #root.target
targetClass root object 当前被调用的目标对象类 #root.targetClass
args root object 当前被调用的方法的参数列表 #root.args[0]
caches root object 当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个cache #root.caches[0].name
argument name evaluation context 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引; #iban 、 #a0 、 #p0
result evaluation context 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false) #result

这个掌握就好,没有必要去死记硬背,默认情况下的配置都是够用的。

2.缓存使用过程解析

首先需要引入spring-boot-starter-cache依赖

然后使用@EnableCaching开启缓存功能

然后就可以使用缓存注解来支持了。

先看一下官方API里面是怎么说的吧:

@Target(value=TYPE)
@Retention(value=RUNTIME)
@Documented
@Import(value=CachingConfigurationSelector.class)
public @interface EnableCaching

Enables Spring's annotation-driven cache management capability,To be used together with @Configuration classes as follows:

@Configuration
@EnableCaching
public class AppConfig {

    @Bean
    public MyService myService() {
        // configure and return a class having @Cacheable methods
        return new MyService();
    }
    @Bean
    public CacheManager cacheManager() {
        // configure and return an implementation of Spring's CacheManager SPI
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
        return cacheManager;
    }
}

@EnableCaching is responsible for registering the necessary Spring components that power annotation-driven cache management, such as the CacheInterceptor and the proxy- or AspectJ-based advice that weaves the interceptor into the call stack when @Cacheable methods are invoked.

官方文档的描述简洁明了,我们只需要开启缓存,然后定制CacheManager即可。

If the JSR-107 API and Spring's JCache implementation are present, the necessary components to manage standard cache annotations are also registered. This creates the proxy- or AspectJ-based advice that weaves the interceptor into the call stack when methods annotated with CacheResult, CachePut, CacheRemove or CacheRemoveAll are invoked.

强大的spring同样支持了JSR107缓存注解!!!当然,本文还是主要以讲解spring的缓存注解为主。

For those that wish to establish a more direct relationship between @EnableCaching and the exact cache manager bean to be used, the CachingConfigurer callback interface may be implemented. Notice the @Override-annotated methods below:

如果想要明确地定制你的CacheManager,可以像下面这样使用

 @Configuration
 @EnableCaching
 public class AppConfig extends CachingConfigurerSupport {

     @Bean
     public MyService myService() {
         // configure and return a class having @Cacheable methods
         return new MyService();
     }

     @Bean
     @Override
     public CacheManager cacheManager() {
         // configure and return an implementation of Spring's CacheManager SPI
         SimpleCacheManager cacheManager = new SimpleCacheManager();
         cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
         return cacheManager;
     }

     @Bean
     @Override
     public KeyGenerator keyGenerator() {
         // configure and return an implementation of Spring's KeyGenerator SPI
         return new MyKeyGenerator();
     }
 }

This approach may be desirable simply because it is more explicit, or it may be necessary in order to distinguish between two CacheManager

因为一个应用环境下可以有多个CacheManager,这样声明CacheManager可以更加直观。

Notice also the keyGenerator method in the example above. This allows for customizing the strategy for cache key generation, per Spring's KeyGenerator SPI. Normally, @EnableCaching will configure Spring's SimpleKeyGenerator for this purpose, but when implementing CachingConfigurer, a key generator must be provided explicitly. Return null or new SimpleKeyGenerator() from this method if no customization is necessary.

如果实现了CachingConfigurer接口,就需要明确定义keyGenerator

CachingConfigurer offers additional customization options: it is recommended to extend from CachingConfigurerSupport that provides a default implementation for all methods which can be useful if you do not need to customize everything. See CachingConfigurer Javadoc for further details.

可以通过继承CachingConfigurerSupport来实现其它的定制功能。CachingConfigurerSupport类的结构如下,可以只对你需要定制的功能进行重写,其它的一律默认返回null即可,如果返回null,那么spring boot 的自动配置就会生效。


/**
 * An implementation of {@link CachingConfigurer} with empty methods allowing
 * sub-classes to override only the methods they're interested in.
 *
 * @author Stephane Nicoll
 * @since 4.1
 * @see CachingConfigurer
 */
public class CachingConfigurerSupport implements CachingConfigurer {

    @Override
    public CacheManager cacheManager() {
        return null;
    }

    @Override
    public KeyGenerator keyGenerator() {
        return null;
    }

    @Override
    public CacheResolver cacheResolver() {
        return null;
    }

    @Override
    public CacheErrorHandler errorHandler() {
        return null;
    }

}

The mode() attribute controls how advice is applied: If the mode is AdviceMode.PROXY (the default), then the other attributes control the behavior of the proxying. Please note that proxy mode allows for interception of calls through the proxy only; local calls within the same class cannot get intercepted that way.

Note that if the mode() is set to AdviceMode.ASPECTJ, then the value of the proxyTargetClass() attribute will be ignored. Note also that in this case the spring-aspects module JAR must be present on the classpath, with compile-time weaving or load-time weaving applying the aspect to the affected classes. There is no proxy involved in such a scenario; local calls will be intercepted as well.

真是纵享丝滑。

3.实际上手

@CacheConfig注解可以定义当前类的所有使用到缓存注解(@Cacheable,@CachePut,@CacheEvict)的通用配置,下面的示例代码实际只配置了当前类的缓存名称

@Service
@CacheConfig(cacheNames = "student")
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    @Cacheable
    public Student getStudentById(Integer id) {
        System.out.println("从数据库中查询学生:" + id);
        return studentMapper.getStudentById(id);
    }

    @CachePut
    public Student updateStudent(Student student) {
        System.out.println("更新数据库中的学生数据:" + student);
        studentMapper.updateStudent(student);
        return student;
    }

    @CacheEvict
    public void deleteStudent(Integer id) {
        System.out.println("删除数据库中的学生:"+id);
        studentMapper.delStudent(id);
    }
}

上面只是简单的使用这三个注解,更加详细的属性使用,请看后面的内容。我们先测试一下缓存的使用效果。

测试类的代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoCacheApplicationTests {
    @Autowired
    private StudentService studentService;

    @Test
    public void contextLoads() {
        System.out.println(studentService.getStudentById(1));
    }

    @Test
    public void testUpdate() {
        studentService.updateStudent(new Student(1,"gotohell","female",23));
    }

    @Test
    public void testDelete() {
        studentService.deleteStudent(1);
    }
}

首先测试@Cacheable注解,第一次调用该方法,打印的日志如下:

从数据库中查询学生:1
student{id=1, name='mmm', gender='male', age=21}

第二次调用该方法,打印的日志如下:

student{id=1, name='mmm', gender='male', age=21}

说明缓存已经生效了,没有从数据库中获取学生数据。我们看一下缓存里面的内容,

Snipaste_2019-09-26_22-52-46.png

这是默认使用jdk序列化存储的结果,我们可以选择采用json格式存储数据。另外,key的生成策略,默认是cache名称前缀加上方法参数,我觉得这个默认情况下就已经够用了,不需要再进行额外的定制。

再来测试一下修改,

打印日志如下:

更新数据库中的学生数据:student{id=1, name='gotohell', gender='female', age=23}

查看数据库中的数据,已经修改成功,redis的数据由于是序列化的,这里就不截图了,我们直接再调用一次查询看它有没有更新即可。

打印结果如下:

student{id=1, name='mmm', gender='male', age=21}

Description does not update data in the cache, is it @CachePut comment does not work it?

Look redis

Snipaste_2019-09-26_23-01-43.png

Discovered that the original second revision of data, use the default cache key objects, why, because by default, the key strategy is to generate parameter caching name student + method, and the parameter update method is that students object, testing can not get the data after the update, because the two are inconsistent key.

So long as the key is specified as an update method can not yet

@CachePut(key = "#result.id")
public Student updateStudent(Student student) {
    System.out.println("更新数据库中的学生数据:" + student);
    studentMapper.updateStudent(student);
    return student;
}

Reassign the key is like this, it supports expressions spring, the specific usage rules, the table listed previously. After re-test, the print log is as follows:

student{id=1, name='gotohell', gender='female', age=23}

To get the data after the update, indicating a key role.

Again test delete, print log is as follows:

删除数据库中的学生:1

Data in the database has been successfully removed, the data cache has also been cleared.

Snipaste_2019-09-26_23-11-59.png

This time again calling query, print the log is as follows:

从数据库中查询学生:1
null

Printing from the log of view, query the database because the cache and gone, but the data in the database is deleted, it returns null

4. JSON objects are serialized

This requires us to customize CacheManager, and add a new configuration class

@Configuration
public class MyRedisConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        //初始化一个RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        //设置CacheManager的值序列化方式为json序列化
        RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer());
        RedisCacheConfiguration defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(pair).entryTtl(Duration.ofHours(1));
        //初始化RedisCacheManager
        return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
    }
}

Retest query methods, we found a cache of values ​​using JSON serialization format.

Snipaste_2019-09-26_23-46-09.png

Source address: https: //github.com/lingEric/springboot-integration-hello

Guess you like

Origin www.cnblogs.com/ericling/p/11595222.html
Recommended