Spring Boot與緩存

 

 本文主要介紹  JSR107 ,Spring緩存抽象、整合redis

 1.  JSR107 

 javaee發佈了 JSR107緩存規範,其中定義了5個核心接口,分別是 CachingProvider ,CacheManager、Cache、Entry和Expiry

  • CachingProvider定義了創建、配置、獲取、管理和控制多個CacheManager。一個應用可以在運行期訪問多個CachingProvider。
  • CacheManager 定義了創建、配置、獲取、管理和控制多個唯一命名的Cache,這些Cache存在於CacheManager的上下文中。一個CacheManager僅被一個CachingProvider所擁有。
  • Cache是一個類似於Map的數據結構並臨時存儲以Key爲索引的值、一個Cache僅被一個CacheManager所擁有。
  • Entry 是一個存儲在Cache中的key-value對
  • Expiry每一個存儲在Cache中的條目有一個定義的有效期,一旦超過這個時間,條目爲過期的狀態。一旦過期,條目將不可訪問、更新和刪除。緩存有效期可以通過ExpiryPolicy設置。

 2. Spring緩存抽象

 由於後來整合要使用jsr107,整個系統難度較大等一系列原因,導致用的比較少,我們更多使用的是spring緩存抽象,其底層卻是和jsr107一樣。

Spring從3.1開始定義了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口來統一不同的緩存技術,並且支持使用JCache(JSR-107)註解簡化我們開發。

Cache

緩存接口,定義緩存操作。實現有:RedisCache、EnCacheCache、ConcurrentMapCache等

CacheManager 緩存管理器,管理各種緩存(Cache)組件
@Cacheable 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存
@CacheEvict 清空緩存
@CachePut 緩存更新,保證方法被調用,又希望結果被緩存時使用該註解
@EnableCaching 開啓基於註解的緩存
keyGenerator 緩存數據時key生成策略
serialize 緩存數據時value序列化策略
  •  Cache接口爲緩存的組件規範定義,包含緩存的各種操作集合。
  • Cache接口下Spring提供了各種xxxCache的實現,如RedisCache、EhCacheCache、ConcurrentMapCache等。
  • 每次調用需要緩存功能的方法時,Spring會檢查指定參數的指定的目標方法是否已經被調用過,如果有就直接從緩存中獲取方法調用後的結果,如果沒有就調用方法並緩存結果後返回給用戶。下次調用直接從緩存中獲取。
  • 使用spring緩存抽象時,需要關注以下兩點
  1.    確定方法需要被緩存以及他們的緩存策略。
  2.    從緩存中讀取之前緩存存儲的數據。

搭建緩存測試基本環境:

 數據庫表創建 

/*
Navicat MySQL Data Transfer

Source Server         : 本地
Source Server Version : 50528
Source Host           : 127.0.0.1:3306
Source Database       : springboot_cache

Target Server Type    : MYSQL
Target Server Version : 50528
File Encoding         : 65001

Date: 2018-10-29 10:54:04
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for department
-- ----------------------------
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `departmentName` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `lastName` varchar(255) DEFAULT NULL,
  `email` varchar(255) DEFAULT NULL,
  `gender` int(2) DEFAULT NULL,
  `d_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  創建一個新工程,引入以下依賴(idea中):

        <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>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

創建javabean對象

  

public class Department {
    private Integer id;
    private String departmentName;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getDepartmentName() {
        return departmentName;
    }

    public void setDepartmentName(String departmentName) {
        this.departmentName = departmentName;
    }

    public Department() {
        super();
    }

    public Department(Integer id, String departmentName) {
        this();
        this.id = id;
        this.departmentName = departmentName;
    }

    @Override
    public String toString() {
        return "Department[" +
                "id=" + id +
                ", departmentName='" + departmentName + '\'' +
                ']';
    }
}
public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    private Integer gender; //性別 1男 0女
    private Integer dId;

    public Employee() {
    }

    public Employee(Integer id, String lastName, String email, Integer gender, Integer dId) {
        this();
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.dId = dId;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    public Integer getdId() {
        return dId;
    }

    public void setdId(Integer dId) {
        this.dId = dId;
    }

    @Override
    public String toString() {
        return "Employee[" +
                "id=" + id +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                ", gender=" + gender +
                ", dId=" + dId +
                ']';
    }
}

配置數據源:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_cache
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver  # 可以省略不寫,可根據連接自動判斷
mybatis:
  configuration:  # 開啓駝峯命名法
    map-underscore-to-camel-case: true

使用註解版的mybatis

   1) @MapperScan指定需要掃描的mapper接口所在的包

   

2)mapper文件編寫

@Mapper
public interface EmployeeMapper {
    @Select("SELECT * FROM  employee WHERE id=#{id}")
   public Employee getEmpById(Integer id);

    @Update("UPDATE employee SET lastName=#{lastName},email=#{email},gender=#{gender},d_id=#{dId} WHERE id=#{id}")
    public void update(Employee employee);

    @Delete("DELETE FROM employee WHERE id=#{id}")
    public void deleteEmpById(Integer id);

    @Insert("INSERT INTO employee(lastName,email,gender,d_id) VALUES(#{lastName},#{email},#{gender},#{dId})")
    public void insertEmp(Employee employee);
}

3)service層代碼編寫

@Service
public class EmployeeService {
    @Autowired
    EmployeeMapper employeeMapper;

    public Employee getEmp(Integer id) {
        System.out.println("查詢 " + id + " 號員工");
        Employee emp = employeeMapper.getEmpById(id);
        return emp;

    }
}

 4)controller層代碼編寫

@RestController
@RequestMapping("emp")
public class EmployeeController {
    @Autowired
    private EmployeeService employeeService;

    @GetMapping("/{id}")
    public Employee getEmp(@PathVariable("id") Integer id) {
        return employeeService.getEmp(id);
    }
}

啓動項目測試: localhost:8080/emp/1  ,如下所示

測試環境搭建完畢後,就可以體驗緩存的使用了。 其步驟如下:

  1. 開啓基於註解的緩存  @EnableCaching

  2. 標註緩存註解即可

   @Cacheable  @CacheEvict    @CachePut

我們可以先查看沒有加緩存的情況:

   在application.yml中,打開日誌輸出

logging:
  level:
    com.zhao.springboot.mapper: debug

      瀏覽器訪問 http://localhost:8080/emp/1  ,每刷新一次頁面,就會查詢一次數據庫,如下所示:

這就說明現在是沒有緩存的。下面使用緩存:

  在service層的該查詢方法上個加入   @Cacheable ,將方法的運行結果進行緩存,以後再要相同的數據,直接從緩存中獲取,不用調用方法。

 

@Cacheable(cacheNames={"emp"}) 
 public Employee getEmp(Integer id) {
        System.out.println("查詢 " + id + " 號員工");
        Employee emp = employeeMapper.getEmpById(id);
        return emp;

    }

 重新啓動項目,訪問  http://localhost:8080/emp/1 ,經過幾次刷新,發現控制檯只有一次打印,訪問http://localhost:8080/emp/2發現有新的內容輸出。說明結果已被緩存。

那麼,是什麼樣的一個運行流程呢?

 @Cacheable

 1. 方法運行前,先去查詢Cache(緩存組件),按照cacheName指定的名字獲取(CacheManager先獲取相應的緩存。第一次獲取緩存如果沒有Cache組件會自動創建)

2. 去Cache中查找緩存的內容,使用一個key,默認就是方法的參數。

  key是按照某種策略生成的, 默認是使用keyGenerator生成的,默認使用SimpleKeyGenerator生成key

   SimpleKeyGenerator生成key的默認策略

    如果沒有參數:key=new SimpleKey()

   如果有一個參數:key=參數的值

  如果有多個參數:key=new  SimpleKey(params)

3. 沒有查到緩存就調用目標方法

4. 將目標方法返回的結果,放進緩存中

@Cacheable標註的方法執行前先來檢查緩存中有沒有該數據,默認按照參數的值作爲key去查詢緩存,如果沒有就運行方法並將結果放入緩存,以後再來調用就可以直接使用緩存中的數據

  核心:

   1) 使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】組件

  2)  key使用keyGenerator生成的,默認是SimpleKeyGenerator,也可以自定義。

@CachePut 即調用方法,又更新緩存數據

運行時機:  

   1). 先調用目標方法

   2). 將目標方法的結果緩存起來

 編寫service層的修改員工方法

 @CachePut(value = "emp")
    public Employee update(Employee employee) {
        System.out.println("更新" + employee.getId() + "號員工");
        employeeMapper.update(employee);
        return employee;
    }

如上代碼所示,修改員工信息方法,上面加入註解 @CachePut ,爲了方便起見,在controller層,使用了@PutMapping註解,進行測試如下:

     1)查詢1號員工:查詢的結果會放入到緩存中。

     2) 再次查詢,查詢結果還是之前的結果lastName=張三,gender=1

     3) 更新1號員工 [lastName=zhangsan,gender=0],更新成功後返回的值如下:

 4) 查詢1號員工,查詢結果如下:

 查詢結果卻是沒有更新前的,其原因如下:更新後,確實將返回的結果也放進緩存了,但是由於沒有指定key的名稱,所以默認key爲傳入的employee對象,值:返回的employee對象。於是再次查詢,並沒有去查數據庫,造成數據不一致。

修改如下:  指定key的名稱爲 id

       @CachePut(value = "emp", key = "#result.id")
//        @CachePut(value = "emp",key = "#employee.id")// 兩者都可以
        public Employee update(Employee employee) {
            System.out.println("更新" + employee.getId() + "號員工");
            employeeMapper.update(employee);
            return employee;
        }

  注意:使用result.id 或者employee.id都可以,但是result不能再@Cacheable下使用。執行時機不同。

此時重修更新員工信息後,再次查詢數據就一致了,另外,注意,更新後再去查詢,也並沒有查詢數據庫,而是查詢的緩存。

也就是說 @CachePut即更新了數據庫,也更新了緩存。

@CacheEvict :緩存清除

編寫service中的delete方法 ,並編寫controller中的delete方法 ,如下:

@CacheEvict(value = "emp",key = "#id",allEntries = false,beforeInvocation = false)
        public void deleteEmp(Integer id){
            System.out.println("delete "+id+"號員工");
            employeeMapper.deleteEmpById(id);
        }
    @GetMapping("delete")
    public String delete(Integer id) {
        employeeService.deleteEmp(id);
        return "success";
    }

key:指定要清除的數據

allEntries = false,  allEntries:指定清除這個緩存(value指定的)中所有的數據,默認爲false

 當指定爲true時,就意味着當刪除了1號員工後,emp下的所有員工(1,2號)的緩存都會被清除。
beforeInvocation = false ,緩存的清除是否在方法執行前執行。

   默認代表是緩存清除操作在方法執行後執行,如果出現異常緩存就不會被清除。 當指定爲true時,意味着,清除緩存操作是在方法執行前執行,無論方法是否出現異常,緩存都清除。

@Caching  :定義複製的緩存註解

在mapper中新增一個方法,根據lastName查詢員工,在service新增方法 ,controller中同樣新增,如下:

 @Select("SELECT * FROM  employee WHERE lastName=#{lastName}")
    public Employee getEmpByLastName(String lastName);
 @Caching(
            cacheable = {
                    @Cacheable(value = "emp", key = "#name")
            },
            put = {
                    @CachePut(value = "emp", key = "#result.id"),
                    @CachePut(value = "emp", key = "#result.email")
            }
    )
    public Employee getEmpByLastName(String name) {
        return employeeMapper.getEmpByLastName(name);
    }
@GetMapping("/lastName/{lastName}")
    public Employee getEmpByLastName(@PathVariable("lastName") String lastName) {
        return employeeService.getEmpByLastName(lastName);
    }

此時注意: 當調用 該方法後,會分別以key爲id,email,lastName將結果放入緩存中,此時如果在根據id查詢, 就直接查詢的數據庫,而不是查數據庫,但是當根據lastName查詢時,發現並沒有從緩存中,而是每次都查詢了數據庫,這是由於上面我們指定了 @CachePut ,該註解要求每次查詢數據庫,並更新緩存。

3. 整合redis

默認使用的是ConcurrentMapCacheManager==ConcurrentMapCache,將數據保存在ConcurrentMap<Object,Object>中,在開發中也可以使用緩存中間件:redis,memcached,ehcache

整合redis ,前提在linux上已經安裝了redis ,關於安裝redis的安裝可以參考這篇文章:redis安裝, 如果需要該工具,可以下載這個:

  redis工具

並通過工具進行連接 ,如下所示:  

 即連接成功。

引入redis的starter :在pom文件中添加以下依賴:

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

配置redis

 application.yml配置文件中新增redis配置: spring.redis.host: 192.168.xxx.xxx

可在測試類中簡單測試redis 。

 @Autowired
    StringRedisTemplate stringRedisTemplate; //操作字符串的
    @Autowired
    RedisTemplate redisTemplate;   // k-v都是對象的

    @Autowired
    RedisTemplate<Object, Employee> empRedisTemplate;

/**
     * Redis常見5大類型
     * String(字符串),list(列表),Set(集合),Hash(散列),ZSet(有序)
     * stringRedisTemplate.opsForValue();[String字符串]
     * stringRedisTemplate.opsForList(); [list集合]
     * stringRedisTemplate.opsForSet(); [Set集合]
     * stringRedisTemplate.opsForHash(); [Hash散列]
     * stringRedisTemplate.opsForZSet(); [ZSet(有序集合)]
     */
    @Test
    public void testRedis() {
        //給redis 中保存數據
//        stringRedisTemplate.opsForValue().append("msg","hello");
        String val=(String )stringRedisTemplate.opsForValue().get("msg");
        System.out.println(val);
        // 測試 list
//        stringRedisTemplate.opsForList().leftPush("myList","1");
//        stringRedisTemplate.opsForList().leftPush("myList","2");
//        stringRedisTemplate.opsForList().leftPush("myList","3");
    }

    //測試保存對象
    @Test
    public void testObj() {
        Employee employee = employeeMapper.getEmpById(1);
        // 默認如果保存對象,使用jdk序列化機智,序列化後的數據保存到redis中
//        redisTemplate.opsForValue().set("emp-01",employee); //保存後,在查看,發現全部是 \xAc之類的,並不是json形式存入的
        // 1. 將數據以json的方式保存
        //2. redisTemplate默認的序列化規則:改變默認的序列化規則,在MyRedisConfig中配置的
        empRedisTemplate.opsForValue().set("emp-01", employee);
    }

注意:當在redis中放入對象時,該對象需要被序列化(實現Serializable接口 ),如果直接用restTempPlate放入(jdk的序列化器),那麼會產生以下的情況 ,並不是以json形式傳入的,

 這是因爲默認使用的是jdk的序列化機制,而我們在日常生產中,更多使用的是json形式的數據,爲此,我們可以自定義一個配置,改變默認的序列化規則(使用json的序列化機制)。配置類如下:

@Configuration
public class MyRedisConfig {
    // 同理,如果要轉換別的對象,可以重新定義新的template。
    @Bean
    public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Employee> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 創建一個json的序列化器
        Jackson2JsonRedisSerializer<Employee> jjrs = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
        template.setDefaultSerializer(jjrs);
        return template;
    }
}

這樣,使用empRedistemplate 再重新存入對象,就變成如下的形式:

 測試緩存:

   原理:CacheManager===Cache 緩存組件來實際給緩存中存取數據

   1) 引入redis的starter ,容器中保存的是RedisCacheManager

   2) RedisCacheManager幫我們自動創建RedisCache來作爲緩存組件,RedisCache通過操作redis來緩存數據。

  3)  默認保存數據k-v都是object,利用序列化保存,如何保存爲json?

       1. 引入redis的starter ,cacheManager變爲RedisCacheManager 

       2. 默認創建的RedisCacheManager操作redis的時候使用的是 jdk的序列化機制(RedisCacheConfiguration.class)

 public static RedisCacheConfiguration defaultCacheConfig() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        registerDefaultConverters(conversionService);
        return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), SerializationPair.fromSerializer(new StringRedisSerializer()), SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()), conversionService);
    }

  3. 自定義CacheManager。

@Bean
    public RedisCacheManager empCacheManager(RedisConnectionFactory factory) {

        Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));

        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
        return redisCacheManager;
    }

 自定義CacheManager後,怎麼使得我們自己的定義的cacheManager生效呢?在RedisCacheConfiguration的註解中可以看到,

 @ConditionalOnMissingBean ,也就是說,如果容器中沒有CacheManager,就使用該配置創建緩存管理器,但是容器中有,就會替換原有的,於是我們就可以直接啓動項目,測試。 如下:

 另外 ,以上全部是通過註解式的將數據放入緩存,也可以通過編碼式的將數據放入緩存,可以在service中引入 自定義的manager,在需要操作緩存的地方,使用manager.getCache(),拿到緩存對象,然後操作數據。

@Service
@CacheConfig(cacheNames = "dep", cacheManager = "deptCacheManager") //因爲存在多個cacheManager ,通過該註解,指明使用某個cacheManager
public class DepartmentService {


    @Qualifier("deptCacheManager")  //精確的根據id名爲deptCacheManager拿到bean注入
    @Autowired
    RedisCacheManager  deptCacheManager;  //根據名稱獲取緩存

    /**
     *   編碼式 存入緩存
     * @param id
     * @return
     */
    public Department getDepById(Integer id){
        System.out.println("查詢部門 " + id);
        Department dept= departmentMapper.getDeptById(id);
         //獲取緩存。
        Cache deptCache= deptCacheManager.getCache("dep");
         //放入緩存
        deptCache.put("dept:"+dept.getId(),dept);
        return  dept ;
    }
}

注意:當容器中存在多個cacheManager時, 要有一個主cacheManager,即用 @Primary 標識的manager,另外不同的service操作時,要指定 其使用的cacheManager。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章