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、介面測試
略