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并配置

Windows 64 位 下载地址

再加上以下脚本文件

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 集群

在windows上搭建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、接口测试



本文为Phil Jing原创文章,未经博主允许不得转载,如有问题请直接回复或者加群。
原文地址:https://www.cnblogs.com/phil_jing/p/15615870.html