1. 程式人生 > >Spring Boot 學習之快取和 NoSQL 篇(四)

Spring Boot 學習之快取和 NoSQL 篇(四)

該系列並非完全原創,官方文件作者

一、前言

當系統的訪問量增大時,相應的資料庫的效能就逐漸下降。但是,大多數請求都是在重複的獲取相同的資料,如果使用快取,將結果資料放入其中可以很大程度上減輕資料庫的負擔,提升系統的響應速度。

本篇將介紹 Spring Boot 中快取和 NoSQL 的使用。上篇文章《Spring Boot 入門之持久層篇(三)》

二、整合EhCache 快取

Spring Boot 針對不同的快取技術實現了不同的封裝,提供了以下幾個註解實現宣告式快取:

@EnableCaching	開啟快取功能,放在配置類或啟動類上
@CacheConfig	快取配置,設定快取名稱
@Cacheable	執行方法前先查詢快取是否有資料。有則直接返回快取資料;否則查詢資料再將資料放入快取
@CachePut	執行新增或更新方法後,將資料放入快取中
@CacheEvict	清除快取
@Caching	將多個快取操作重新組合到一個方法中

1、新增依賴

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

2、新增配置

在 src/main/resources 目錄下建立 ehcache.xml 檔案,內容如下:

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="ehcache.xsd">
    <cache name="question" 
           eternal="false"
           maxEntriesLocalHeap="0" 
           timeToIdleSeconds="50">
    </cache>
</ehcache>

這裡的name可以多個,與@CacheConfig的cacheNames對應

application.properties 新增

spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:ehcache.xml

3、新增快取註解

在前文基礎之上進行修改新增

Service層

package com.phil.springboot.service.impl;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import com.phil.springboot.mappper.QuestionMapper;
import com.phil.springboot.service.QuestionService;

@Service
@CacheConfig(cacheNames = "question")
public class QuestionServiceImpl implements QuestionService {

	@Autowired
	private QuestionMapper questionMapper;
	
	@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
	@Override
	@Cacheable(key = "#params")
	public List<Map<String, Object>> findByPage(Map<String, Object> params) {
		return questionMapper.findByPage(params);
	}
	
	@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
	@Override
	@Cacheable(key = "#params")
	public Map<String, Object> findByProperty(Map<String, Object> params) {
		return questionMapper.findByProperty(params);
	}
	
	@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
	@Override
	@CachePut(key = "#params")
	public Integer saveOrUpdate(Map<String, Object> params){
		Integer i = 0;
		if (StringUtils.isEmpty(params.get("id"))) {
			i = questionMapper.save(params);
		} else {
			i = questionMapper.update(params);
			i ++;
		}
		return i;
	}
	
	@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
	@CacheEvict(key = "#ids")
	@Override
	public Integer delete(String ids){
		if(StringUtils.isEmpty(ids)){
			return -1;
		}
		String[] strs = ids.trim().split(",");
		Integer[] ids_ = new Integer[strs.length];
		for(int i = 0; i < strs.length; i++){
			ids_[i] = Integer.parseInt(strs[i]);
		}
		return questionMapper.delete(ids_);
	}
}

控制層

package com.phil.springboot.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.phil.springboot.service.QuestionService;

import io.swagger.annotations.Api;

@Api(value = "問題Rest介面")
@RestController
@RequestMapping("api/question")
public class QuestionController {
	
	private Logger logger = LoggerFactory.getLogger(this.getClass());

	@Autowired
	private QuestionService questionService;
	
	@PostMapping("list")
	public Map<String, Object> list(@RequestBody Map<String, Object> map) {
		Map<String, Object> data = new HashMap<String, Object>();
		List<Map<String, Object>> list;
		try {
			list = questionService.findByPage(map);
			data.put("msg", list);
			data.put("code", 200);
		} catch (Exception e) {
			data.put("msg", e.getMessage());
			data.put("code", -1);
		}
		logger.debug("list {}" , data);
		return data;
	}
	
	@GetMapping("get/{id}")
	public Map<String, Object> get(@PathVariable("id")Integer id) {
		Map<String, Object> data = new HashMap<String, Object>();
		Map<String, Object> params = new HashMap<String, Object>();
		params.put("id", id);
		Map<String, Object> map;
		try {
			map = questionService.findByProperty(params);
			data.put("msg", map);
			data.put("code", 200);
		} catch (Exception e) {
			data.put("msg", e.getMessage());
			data.put("code", -1);
		}
		logger.debug("get {}" , data);
		return data;
	}
	
	@PostMapping("put")
	public Map<String, Object> put(@RequestBody Map<String, Object> map) {
		Map<String, Object> data = new HashMap<String, Object>();
		Integer i = questionService.saveOrUpdate(map);
		logger.debug("put status {}" , i);
		if(i == 1){
			data.put("msg", "新增成功");
			data.put("code", 200);
		} else if (i == 2) {
			data.put("msg", "修改成功");
			data.put("code", 200);
		} else {
			data.put("msg", "資料處理失敗");
			data.put("code", -1);
		}
		logger.debug("put {}" , data);
		return data;
	}

	@PostMapping("delete")
	public Map<String, Object> delete(@RequestBody String ids) {
		Map<String, Object> data = new HashMap<String, Object>();
		Integer i = questionService.delete(ids);
		logger.debug("delete {}" , i);
		if(i > 0){
			data.put("msg", "刪除成功");
			data.put("code", 200);
		} else {
			data.put("msg", "刪除失敗");
			data.put("code", -1);
		}
		logger.debug("delete {}" , data);
		return data;
	}
}
啟動類

新增 @EnableCaching 註解,開啟快取功能

4、介面測試

1)List :http://localhost:8081/api/question/list

連續發起兩次list請求

2018-04-04 16:23:40.807 |-DEBUG [http-nio-8081-exec-2] com.phil.springboot.mappper.QuestionMapper.findByPage [159] -| ==>  Preparing: select id, number, description from question 
2018-04-04 16:23:40.808 |-DEBUG [http-nio-8081-exec-2] com.phil.springboot.mappper.QuestionMapper.findByPage [159] -| ==> Parameters: 
2018-04-04 16:23:40.810 |-DEBUG [http-nio-8081-exec-2] com.phil.springboot.mappper.QuestionMapper.findByPage [159] -| <==      Total: 15
2018-04-04 16:23:40.811 |-DEBUG [http-nio-8081-exec-2] com.phil.springboot.controller.QuestionController [43] -| list {code=200, msg=[{id=24, description=問題三不需要描述了, number=3}, {id=25, description=問題四描述, number=4}, {id=27, description=問題四描述, number=4}, {id=29, description=問題二描述, number=2}, {id=30, description=問題三描述, number=3}, {id=31, description=問題四描述, number=4}, {id=32, description=問題40描述, number=40}, {id=33, description=問題63描述, number=63}, {id=36, description=問題87描述, number=87}, {id=39, description=新問題, number=6}, {id=40, description=新問題, number=6}, {id=41, description=新問題, number=8}, {id=42, description=新問題, number=118}, {id=43, description=新問題, number=119}, {id=44, description=新問題, number=119}]}
2018-04-04 16:23:44.887 |-DEBUG [http-nio-8081-exec-3] com.phil.springboot.controller.QuestionController [43] -| list {code=200, msg=[{id=24, description=問題三不需要描述了, number=3}, {id=25, description=問題四描述, number=4}, {id=27, description=問題四描述, number=4}, {id=29, description=問題二描述, number=2}, {id=30, description=問題三描述, number=3}, {id=31, description=問題四描述, number=4}, {id=32, description=問題40描述, number=40}, {id=33, description=問題63描述, number=63}, {id=36, description=問題87描述, number=87}, {id=39, description=新問題, number=6}, {id=40, description=新問題, number=6}, {id=41, description=新問題, number=8}, {id=42, description=新問題, number=118}, {id=43, description=新問題, number=119}, {id=44, description=新問題, number=119}]}

2)http://localhost:8081/api/question/get/24

連續發起兩次get請求

2018-04-04 16:25:52.984 |-DEBUG [http-nio-8081-exec-6] com.phil.springboot.mappper.QuestionMapper.findByProperty [159] -| ==>  Preparing: select id, number, description from question WHERE id = ? 
2018-04-04 16:25:52.985 |-DEBUG [http-nio-8081-exec-6] com.phil.springboot.mappper.QuestionMapper.findByProperty [159] -| ==> Parameters: 24(Integer)
2018-04-04 16:25:52.986 |-DEBUG [http-nio-8081-exec-6] com.phil.springboot.mappper.QuestionMapper.findByProperty [159] -| <==      Total: 1
2018-04-04 16:25:52.987 |-DEBUG [http-nio-8081-exec-6] com.phil.springboot.controller.QuestionController [61] -| get {code=200, msg={id=24, description=問題三不需要描述了, number=3}}
2018-04-04 16:25:55.310 |-DEBUG [http-nio-8081-exec-7] com.phil.springboot.controller.QuestionController [61] -| get {code=200, msg={id=24, description=問題三不需要描述了, number=3}}

3)http://localhost:8081/api/question/put

{
  "description": "新問題",
  "number": 150
}
發起儲存
2018-04-04 16:27:28.300 |-DEBUG [http-nio-8081-exec-10] com.phil.springboot.mappper.QuestionMapper.save [159] -| ==>  Preparing: insert into question (number,description) values (?, ?) 
2018-04-04 16:27:28.301 |-DEBUG [http-nio-8081-exec-10] com.phil.springboot.mappper.QuestionMapper.save [159] -| ==> Parameters: 150.0(Double), 新問題(String)
2018-04-04 16:27:28.302 |-DEBUG [http-nio-8081-exec-10] com.phil.springboot.mappper.QuestionMapper.save [159] -| <==    Updates: 1
2018-04-04 16:27:28.306 |-DEBUG [http-nio-8081-exec-10] com.phil.springboot.controller.QuestionController [79] -| save {code=200, msg=新增成功}

4)http://localhost:8081/api/question/put

 {
      "id": 24,
      "description": "問題三三三不需要描述了",
      "number": 333
}

發起update

2018-04-04 16:55:26.791 |-DEBUG [http-nio-8081-exec-4] com.phil.springboot.mappper.QuestionMapper.update [159] -| ==>  Preparing: update question set number = ?, description = ? where id = ? 
2018-04-04 16:55:26.791 |-DEBUG [http-nio-8081-exec-4] com.phil.springboot.mappper.QuestionMapper.update [159] -| ==> Parameters: 333.0(Double), 問題三三三不需要描述了(String), 24.0(Double)
2018-04-04 16:55:26.793 |-DEBUG [http-nio-8081-exec-4] com.phil.springboot.mappper.QuestionMapper.update [159] -| <==    Updates: 1

沒有日誌列印,但返回修改後的物件資料,說明快取中的資料已經同步。(增加了一個新的Key)

目前發現個bug,gson會把int long自動轉換為double

三、整合Redis 快取

1、新增依賴

<!-- Redis 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、安裝Redis並配置

再加上以下指令碼檔案

startup.bat

redis-server.exe redis.windows.conf  

service-install.bat

redis-server.exe --service-install redis.windows.conf --loglevel verbose  

uninstall-service.bat

redis-server --service-uninstall  

查詢Redis所有Key的命令

redis 127.0.0.1:6379> KEYS *

application-local.properties新增redis的配置(檢視RedisProperties.class原始碼,部分已經預設)

#spring.redis.host=localhost
spring.redis.password=
#spring.redis.port=6379
spring.redis.timeout=3000

把原來ehcache.xml rename為ehcache.xml--,啟動類的@EnableCaching去除就可以完全使用Redis快取。

3、建立配置類

package com.phil.springboot.config;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
@EnableCaching
public class RedisCacheManagerConfig {

//	@Bean
//	@ConditionalOnMissingBean(name = "redisTemplate")
//	public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory)
//			throws UnknownHostException {
//		RedisTemplate<?, ?> template = new RedisTemplate<>();
//		template.setConnectionFactory(redisConnectionFactory);
//		return template;
//	}

	@Bean
//	public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
	public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
		/**1.x寫法 
		RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
		//cacheManager.setDefaultExpiration(3000); // =Sets the default expire time
		 */
		//2.x寫法
		//question資訊快取配置
	    RedisCacheConfiguration questionCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)).disableCachingNullValues().prefixKeysWith("question");
	    Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
	    redisCacheConfigurationMap.put("question", questionCacheConfiguration);
	    
	    //初始化一個RedisCacheWriter
	    RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
	    
	    //設定CacheManager的值序列化方式為JdkSerializationRedisSerializer,但其實RedisCacheConfiguration預設就是使用StringRedisSerializer序列化key,JdkSerializationRedisSerializer序列化value,所以以下注釋程式碼為預設實現
	    //ClassLoader loader = this.getClass().getClassLoader();
	    //JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer(loader);
	    //RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jdkSerializer);
	    //RedisCacheConfiguration defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);   
	    RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
	    //設定預設超過期時間是30秒
	    defaultCacheConfig.entryTtl(Duration.ofSeconds(30));
	    //初始化RedisCacheManager
	    RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig, redisCacheConfigurationMap);
		return cacheManager;
	}
	
}

在專案中可以直接引用RedisTemplate 和 StringRedisTemplate 兩個模板進行資料操作,或者自定義封裝API

4、介面測試

略,同上(可通過查詢Redis所有Key的命令發現快取的Key變化)

5、Redis測試

package com.phil.springboot.redis;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTest {

	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	@Test
	public void testSet() {
		String key = "name";
		String value = "zhangsan";
		stringRedisTemplate.opsForValue().set(key, value);
	}

	@Test
	public void testGet() {
		String key = "name";
		String value = stringRedisTemplate.opsForValue().get(key);
		System.out.println(value);
	}

	@Test
	public void testDelete() {
		String key = "name";
		stringRedisTemplate.delete(key);
	}
}

四、整合Redis 叢集

1、新增依賴

在3.1基礎之上,在pom.xml繼續新增

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
</dependency>

2、配置檔案

在application-local.properties新增以下配置

spring.redis.pool.max-idle=8
spring.redis.pool.max-wait=-1
spring.redis.cluster.nodes=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384
spring.redis.commandTimeout=5000

3、配置類

package com.phil.springboot.config;

import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
@EnableCaching
public class RedisCacheManagerConfig {

	@Value("${spring.redis.cluster.nodes}")
	private String clusterNodes;

	@Value("${spring.redis.pool.max-idle}")
	private int maxIdle;

	@Value("${spring.redis.pool.max-wait}")
	private int maxWait;

	@Value("${spring.redis.commandTimeout}")
	private int commandTimeout;

	// @Bean
	// @ConditionalOnMissingBean(name = "redisTemplate")
	// public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory
	// redisConnectionFactory)
	// throws UnknownHostException {
	// RedisTemplate<?, ?> template = new RedisTemplate<>();
	// template.setConnectionFactory(redisConnectionFactory);
	// return template;
	// }

	@Bean
	public JedisCluster getJedisCluster() {
		String[] c_nodes = clusterNodes.split(",");
		Set<HostAndPort> nodes = new HashSet<>();
		// 分割叢集節點
		for (String node : c_nodes) {
			String[] h = node.split(":");
			nodes.add(new HostAndPort(h[0].trim(), Integer.parseInt(h[1].trim())));
			System.err.println("h[1] = " + h[1].trim());
		}
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxIdle(maxIdle); //預設是8
		jedisPoolConfig.setMaxWaitMillis(maxWait);//預設是-1
		return new JedisCluster(nodes, commandTimeout, jedisPoolConfig);
	}

	@Bean
	// public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
	public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
		/**
		 * 1.x寫法 RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
		 * //cacheManager.setDefaultExpiration(3000); // =Sets the default expire time
		 */
		// 2.x寫法
		// question資訊快取配置
		// RedisClusterConnection redisClusterConnection = new
		// JedisClusterConnection(getJedisCluster());
		RedisCacheConfiguration questionCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
				.entryTtl(Duration.ofMinutes(30)).disableCachingNullValues().prefixKeysWith("question");
		Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
		redisCacheConfigurationMap.put("question", questionCacheConfiguration);
		System.err.println("question 快取 啟動");
		// 初始化一個RedisCacheWriter
		RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
		// 設定CacheManager的值序列化方式為JdkSerializationRedisSerializer,但其實RedisCacheConfiguration預設就是使用StringRedisSerializer序列化key,JdkSerializationRedisSerializer序列化value,所以以下注釋程式碼為預設實現
		// ClassLoader loader = this.getClass().getClassLoader();
		// JdkSerializationRedisSerializer jdkSerializer = new
		// JdkSerializationRedisSerializer(loader);
		// RedisSerializationContext.SerializationPair<Object> pair =
		// RedisSerializationContext.SerializationPair.fromSerializer(jdkSerializer);
		// RedisCacheConfiguration
		// defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
		RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
		// 設定預設超過期時間是30秒
		defaultCacheConfig.entryTtl(Duration.ofSeconds(30));
		// 初始化RedisCacheManager
		RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig,
				redisCacheConfigurationMap);
		return cacheManager;
	}
}

4、介面測試