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、接口测试
略