SpringBoot整合Redis + SpringCache + Protobuf,优雅地实现key-value键值对存储DEMO。

前情提要

前些时间张同学问怎么在SpringBoot里整合Redis…,由于很久后才看到消息,于是…

我:你用Redis是想存储什么数据,整出来没
张同学:整出来了,我存key-value的,存用户token信息。

于是我想了解一下他是怎么整合Redis的

我:你咋整合的,给我说道说道...
张同学:就用RedisTemplate啊,哪里需要缓存就哪里添加一下,很简单。

看到这里我…,Spring为键值对的存储提供了一套标准接口,各个类库只要对这些标准接口做实现,那么就可以直接使用org.springframework.cache.annotation包下的注解做缓存了,避免了代码入侵。

我:怎么不用@Cacheable这套注解...redis有整合,可以直接用上
我:既省事又没有代码入侵,总不能哪里用缓存我就手写一下查存一下数据吧...
张同学:。。。我都不知道有这些标准,现在框架都搭好了。

太懒了啊…
自己手动搭了一套SpringBoot + Redis + SpringCache的demo,考虑到序列化,我打算用上Protostuff,它的特点是:基于Protobuf、高效的编解码性能、以及和其他序列化框架相比Protostuff在序列化后字节码流更小,更易于传输及存储。
接下来进入正文…

版本

jdk8SpringBoot 2.1.9.RELEASE

项目源码

本文所用源码已经上传github。

核心类库导入

Redis相关类库

		<!-- apache 连接池 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>
		
		<!-- Redis -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

Protostuff 类库

		<!-- Protostuff 序列化&反序列化工具 -->
		<dependency>
			<groupId>io.protostuff</groupId>
			<artifactId>protostuff-core</artifactId>
			<version>1.6.0</version>
		</dependency>

		<dependency>
			<groupId>io.protostuff</groupId>
			<artifactId>protostuff-runtime</artifactId>
			<version>1.6.0</version>
		</dependency>

Redis核心配置文件

spring:
  cache:
    type: redis
    redis:
      time-to-live: ${REDIS_CACHE_TTL:1800000}
  redis:
    host: ${REDIS_CACHE_HOST:www.yorozuyas.com}
    port: ${REDIS_CACHE_PORT:6379}
    password: ${REDIS_CACHE_PASSWORD:password}

这里只做了简单的配置,其他相关配置自行查看源码或相关资料进行更改。

定义自己的序列化器

透过源码,可以发现每个序列化器都是继承自org.springframework.data.redis.serializer.RedisSerializer接口

package org.springframework.data.redis.serializer;
import org.springframework.lang.Nullable;
...

public interface RedisSerializer<T> {

	/**
	 * Serialize the given object to binary data.
	 */
	@Nullable
	byte[] serialize(@Nullable T t) throws SerializationException;

	/**
	 * Deserialize an object from the given binary data.
	 */
	@Nullable
	T deserialize(@Nullable byte[] bytes) throws SerializationException;
	
	...
}

因此只需要自定义一个继承自该接口的类,并实现serializedeserialize即可。

package com.yorozuyas.demo.cache;

import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import io.protostuff.LinkedBuffer;
import io.protostuff.ProtostuffIOUtil;
import io.protostuff.Schema;
import io.protostuff.runtime.RuntimeSchema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
 * 使用Protostuff对value进行编解码。
 */
public final class ProtostuffRedisSerializer implements RedisSerializer<Object> {

	final Schema<CacheValueWrapper> schema = RuntimeSchema.getSchema( CacheValueWrapper.class );

	@Override
	public byte[] serialize( Object cvw ) throws SerializationException {

		try {
			return encode( cvw );
		}
		catch ( RuntimeException e ) {
			throw new SerializationException( "Could not encode CacheValueWrapper to Protostuff: "
					+ e.getMessage(), e );
		}
	}

	@Override
	public Object deserialize( byte[] bytes ) throws SerializationException {

		try {
			return decode( bytes );
		}
		catch ( RuntimeException e ) {
			throw new SerializationException( "Could not decode Protostuff to CacheValueWrapper: "
					+ e.getMessage(), e );
		}
	}

	// do serialize
	public byte[] encode( Object value ) {

		final LinkedBuffer buffer = LinkedBuffer.allocate();
		return ProtostuffIOUtil.toByteArray( new CacheValueWrapper( value ), schema, buffer );
	}

	// do deserialize
	public Object decode( byte[] bytes ) {

		CacheValueWrapper wrapper = new CacheValueWrapper();

		ProtostuffIOUtil.mergeFrom( bytes, wrapper, schema );
		return wrapper.getData();
	}

	@AllArgsConstructor
	@NoArgsConstructor
	public static class CacheValueWrapper {

		@Getter
		private Object data;
	}
}

如何替换Redis默认的序列化器

在源码org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration这个配置类中,可以看到注入了一个类型为RedisCacheManagerBeanRedisCacheManager在创建过程中,调用determineConfiguration方法,并判断如果已经存在
org.springframework.data.redis.cache.RedisCacheConfiguration这个Bean,则直接返回,否则创建一个默认的org.springframework.data.redis.cache.RedisCacheConfiguration交给RedisCacheManager.

package org.springframework.boot.autoconfigure.cache;
...

@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {

	private final CacheProperties cacheProperties;

	private final CacheManagerCustomizers customizerInvoker;

	private final org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration;

	RedisCacheConfiguration(CacheProperties cacheProperties, CacheManagerCustomizers customizerInvoker,
			ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration) {
		this.cacheProperties = cacheProperties;
		this.customizerInvoker = customizerInvoker;
		this.redisCacheConfiguration = redisCacheConfiguration.getIfAvailable();
	}

	@Bean
	public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
			ResourceLoader resourceLoader) {
		RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory)
				.cacheDefaults(determineConfiguration(resourceLoader.getClassLoader()));
		List<String> cacheNames = this.cacheProperties.getCacheNames();
		if (!cacheNames.isEmpty()) {
			builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
		}
		return this.customizerInvoker.customize(builder.build());
	}

	private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
			ClassLoader classLoader) {
		if (this.redisCacheConfiguration != null) {
			return this.redisCacheConfiguration;
		}
		Redis redisProperties = this.cacheProperties.getRedis();
		org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
				.defaultCacheConfig();
		config = config.serializeValuesWith(
				SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
		if (redisProperties.getTimeToLive() != null) {
			config = config.entryTtl(redisProperties.getTimeToLive());
		}
		if (redisProperties.getKeyPrefix() != null) {
			config = config.prefixKeysWith(redisProperties.getKeyPrefix());
		}
		if (!redisProperties.isCacheNullValues()) {
			config = config.disableCachingNullValues();
		}
		if (!redisProperties.isUseKeyPrefix()) {
			config = config.disableKeyPrefix();
		}
		return config;
	}

}

由于RedisCacheManager负责创建和管理各个RedisCache,并为各个RedisCache提供默认配置,包含序列化器,过期时间等。

package org.springframework.data.redis.cache;
...

public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {

	...
	
	/**
	 * Configuration hook for creating {@link RedisCache} with given name and {@code cacheConfig}.
	 */
	protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
		return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
	}
	...
}

因此我们只需要自定义一个org.springframework.data.redis.cache.RedisCacheConfigurationBean并交由Spring容器即可,后续在启动过程中,RedisCacheManager在创建的时候,会拿到我们注入到Spring中的RedisCacheConfiguration

package com.yorozuyas.demo.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.cache.CacheProperties.Redis;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;

@Configuration
@AutoConfigureAfter(CacheAutoConfiguration.class)
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis", matchIfMissing = false)
public class RedisCacheAutoConfiguration {

	@Autowired
	private CacheProperties cacheProperties;

	/**
	 * Custom {@link org.springframework.data.redis.cache.RedisCacheConfiguration}
	 * 
	 * 
	 * @see org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
	 */
	@Bean
	public RedisCacheConfiguration determineConfiguration() {

		final Redis redisProperties = cacheProperties.getRedis();
		
		// Only open TTL, others close.
		RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
				.disableCachingNullValues()
				.disableKeyPrefix()
				// here, add custom ProtostuffRedisSerializer for value.
				.serializeValuesWith( SerializationPair.fromSerializer( new ProtostuffRedisSerializer() ) );

		if ( redisProperties.getTimeToLive() != null ) {
			config = config.entryTtl( redisProperties.getTimeToLive() );
		}

		return config;
	}
}

模拟场景

  • 在核心类编写完成后,我们来设计一个场景,并将其运用起来。
  • 数据库:MySQL 5.7

CRUD收货地址场景

通常我们在买东西的时候,都需要一个明确的收件地址,而一个用户可以有多个收货地址。

收货地址数据库设计

  • 表名:tb_deliver_address
No field description data type not null
1 id 主键 bigint y
2 uid 用户唯一标识 varchar y
3 name 收货人 varchar y
4 address 收货地址 varchar y
5 mobile 联系方式 bigint y
  • 初始化 sql
DROP TABLE IF EXISTS tb_deliver_address CASCADE;
CREATE TABLE tb_deliver_address (
	id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键id',
	uid varchar(32) NOT NULL COMMENT '用户唯一标识',
	name varchar(24) NOT NULL COMMENT '收货人',
	address varchar(200) NOT NULL COMMENT '收货地址',
	mobile BIGINT NOT NULL COMMENT '联系方式',
	PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO tb_deliver_address(uid, name, address, mobile) VALUES ('zhangsan', '我的姐姐', '湖北省武汉市江汉区发展大道185号', 12345678910);
INSERT INTO tb_deliver_address(uid, name, address, mobile) VALUES ('zhangsan', '我的妹妹', '安徽省合肥市经济技术开发区芙蓉路678号', 12345678911);
INSERT INTO tb_deliver_address(uid, name, address, mobile) VALUES ('zhangsan', '我的弟弟', '浙江省杭州市滨江区江南大道龙湖滨江天街', 12345678912);
INSERT INTO tb_deliver_address(uid, name, address, mobile) VALUES ('zhangsan', '张三', '广东省佛山市顺德区大良迎宾路碧桂园·钻石湾', 12345678913);
  • mapper POJO
package com.yorozuyas.demo.model;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class DeliverAddress {
	
	private Long id;
	
	private String uid;

	private String name;

	private String address;

	private Long mobile;
}

API设计及实现

  • Controller
package com.yorozuyas.demo.controller;
...
import com.yorozuyas.demo.controller.base.BaseResponseEntity;
import com.yorozuyas.demo.enums.Code;
import com.yorozuyas.demo.model.DeliverAddress;
import com.yorozuyas.demo.service.Protobuf2RedisService;

import lombok.extern.slf4j.Slf4j;

@RestController
@Slf4j
@RequestMapping(value = "/address")
public class Protobuf2RedisController {

	public static final String URL_NEW_ADDRESS = "/new";
	public static final String URL_FETCH_ADDRESS = "/fetch";
	public static final String URL_MODIFY_ADDRESS = "/modify";
	public static final String URL_EVICT_ADDRESS = "/evict/{id}";

	@Autowired
	private Protobuf2RedisService protobuf2RedisService;
	
	...

	private void checkIn( DeliverAddress addressInfo ) {
	if ( StringUtils.isNullOrEmpty( addressInfo.getUid() ) )
		throw HttpClientErrorException.create( HttpStatus.BAD_REQUEST, "'uid' must not be null.", null, null,
				StandardCharsets.UTF_8 );
	if ( StringUtils.isNullOrEmpty( addressInfo.getName() ) )
		throw HttpClientErrorException.create( HttpStatus.BAD_REQUEST, "'name' must not be null.", null, null,
				StandardCharsets.UTF_8 );
	if ( StringUtils.isNullOrEmpty( addressInfo.getAddress() ) )
		throw HttpClientErrorException.create( HttpStatus.BAD_REQUEST, "'address' must not be null.", null, null,
				StandardCharsets.UTF_8 );
	if ( addressInfo.getMobile() == null )
		throw HttpClientErrorException.create( HttpStatus.BAD_REQUEST, "'mobile' must not be null.", null, null,
				StandardCharsets.UTF_8 );
	}
}

  • 新增地址
##################################
# 新增地址 
##################################
POST http://>/-path>/address/new
@PostMapping(value = URL_NEW_ADDRESS, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<BaseResponseEntity<String>> newAddress(
		@RequestBody DeliverAddress addressInfo ) {

	log.info( "Request-Method: POST, Request-Path: /new, addressInfo: {}", addressInfo.toString() );

	checkIn( addressInfo );

	protobuf2RedisService.newAddress( addressInfo );

	BaseResponseEntity.BaseResponseEntityBuilder<String> builder = BaseResponseEntity.builder();

	return ResponseEntity.ok()
			.body( builder.code( Code.OK.getCode() )
					.data( "Address added successfully" )
					.build() );

};
  • 查询收货地址
##################################
# 查询收货地址
##################################
GET http://>/-path>/address/fetch
@GetMapping(value = URL_FETCH_ADDRESS, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<BaseResponseEntity<?>> fetchAddress(
		@RequestParam(value = "uid", required = false) String uid ) {

	log.info( "Request-Method: GET, Request-Path: /fetch, uid: {}", uid );

	// check parameter must not be null
	if ( StringUtils.isNullOrEmpty( uid ) ) {
		throw HttpClientErrorException.create( HttpStatus.BAD_REQUEST, "'uid' must not be null.", null, null,
				StandardCharsets.UTF_8 );
	}

	List<DeliverAddress> data = protobuf2RedisService.fetchAddress( uid );

	BaseResponseEntity.BaseResponseEntityBuilder<Object> entityBuilder = BaseResponseEntity.builder()
			.code( Code.OK.getCode() );
	if ( data.isEmpty() ) {
		return ResponseEntity.ok()
				.body( entityBuilder.data( "No result" ).build() );
	}

	return ResponseEntity.ok()
			.body( entityBuilder.data( data ).build() );
}
  • 修改收货地址
##################################
# 修改收货地址
##################################
POST http://>/-path>/address/modify
@PostMapping(value = URL_MODIFY_ADDRESS, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<BaseResponseEntity<String>> modifyAddress(
		@RequestBody DeliverAddress addressInfo ) {

	log.info( "Request-Method: POST, Request-Path: /modify, addressInfo: {}", addressInfo.toString() );

	// check parameter must not be null
	if ( addressInfo.getId() == null )
		throw HttpClientErrorException.create( HttpStatus.BAD_REQUEST, "'id' must not be null.", null, null,
				StandardCharsets.UTF_8 );
	checkIn( addressInfo );

	protobuf2RedisService.modifyAddress( addressInfo );

	BaseResponseEntity.BaseResponseEntityBuilder<String> builder = BaseResponseEntity.builder();

	return ResponseEntity.ok()
			.body( builder.code( Code.OK.getCode() )
					.data( "Address modified successfully" )
					.build() );

}
  • 删除收货地址
##################################
# 删除收货地址
##################################
DELETE http://>/-path>/address/evict/{id}
@DeleteMapping(value = URL_EVICT_ADDRESS, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<BaseResponseEntity<String>> evictAddress(
		@PathVariable(value = "id", required = false) Long id,
		@RequestParam(value = "uid", required = false) String uid ) {

	log.info( "Request-Method: DELETE, Request-Path: /evict, id: {}, uid: {}", id, uid );

	// check parameter must not be null
	if ( id == null )
		throw HttpClientErrorException.create( HttpStatus.BAD_REQUEST, "'id' must not be null.", null, null,
				StandardCharsets.UTF_8 );
	if ( StringUtils.isNullOrEmpty( uid ) )
		throw HttpClientErrorException.create( HttpStatus.BAD_REQUEST, "'uid' must not be null.", null, null,
				StandardCharsets.UTF_8 );

	protobuf2RedisService.evictAddress( id, uid );

	BaseResponseEntity.BaseResponseEntityBuilder<String> builder = BaseResponseEntity.builder();

	return ResponseEntity.ok()
			.body( builder.code( Code.OK.getCode() )
					.data( "Address deleted successfully" )
					.build() );
}

DAO层使用Cache注解

package com.yorozuyas.demo.dao.impl;

...
import com.yorozuyas.demo.dao.Protobuf2RedisDAO;
import com.yorozuyas.demo.model.DeliverAddress;

@Component
public class Protobuf2RedisDAOImpl implements Protobuf2RedisDAO {

	/**
	 * name space
	 */
	private static final String NEW_ADDRESS = "com.yorozuyas.demo.dao.Protobuf2RedisMapper.newAddress";
	private static final String FETCH_ADDRESS = "com.yorozuyas.demo.dao.Protobuf2RedisMapper.fetchAddress";
	private static final String MODIFY_ADDRESS = "com.yorozuyas.demo.dao.Protobuf2RedisMapper.modifyAddress";
	private static final String EVICT_ADDRESS = "com.yorozuyas.demo.dao.Protobuf2RedisMapper.evictAddress";

	@Autowired
	private SqlSessionTemplate sqlSessionTemplate;

	@Override
	@Cacheable(cacheNames = "address", key = "'address:' + #addressInfo.getUid()", unless = "#result.isEmpty()")
	public List<DeliverAddress> newAddress( DeliverAddress addressInfo ) {

		sqlSessionTemplate.insert( NEW_ADDRESS, addressInfo );
		return sqlSessionTemplate.selectList( FETCH_ADDRESS, addressInfo.getUid() );
	}

	@Override
	@Cacheable(cacheNames = "address", key = "'address:' + #uid", unless = "#result.isEmpty()")
	public List<DeliverAddress> fetchAddress( String uid ) {

		return sqlSessionTemplate.selectList( FETCH_ADDRESS, uid );
	}

	@Override
	@CachePut(cacheNames = "address", key = "'address:' + #addressInfo.getUid()", unless = "#result.isEmpty()")
	public List<DeliverAddress> modifyAddress( DeliverAddress addressInfo ) {

		sqlSessionTemplate.update( MODIFY_ADDRESS, addressInfo );
		return sqlSessionTemplate.selectList( FETCH_ADDRESS, addressInfo.getUid() );
	}

	@Override
	@CacheEvict(cacheNames = "address", key = "'address:' + #uid")
	public int evictAddress( Long id, String uid ) {

		final Map<String, Object> map = new HashMap<String, Object>( 2 );
		map.put( "id", id );
		map.put( "uid", uid );

		return sqlSessionTemplate.delete( EVICT_ADDRESS, map );
	}

}

测试

  • 运行redis,执行redis-cli进入redis并执行monitor监视连接请求。
    SpringBoot整合Redis + SpringCache + Protobuf,优雅地实现key-value键值对存储DEMO。_第1张图片

  • 运行MySQL,并执行初始化sql。
    SpringBoot整合Redis + SpringCache + Protobuf,优雅地实现key-value键值对存储DEMO。_第2张图片

  • 运行SpringBoot。

  • 调用查询收货地址api
    SpringBoot整合Redis + SpringCache + Protobuf,优雅地实现key-value键值对存储DEMO。_第3张图片

  • 查看控制台输出信息,输出如下:

2020-07-04 19:43:45.752 14548 --- [ioEventLoop-4-1] DEBUG io.lettuce.core.RedisClient              - Connecting to Redis at www.yorozuyas.com:6379: Success
2020-07-04 19:43:45.754 14548 --- [ioEventLoop-4-1] DEBUG io.lettuce.core.RedisChannelHandler      - dispatching command AsyncCommand [type=AUTH, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.AsyncCommand]
2020-07-04 19:43:45.755 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.DefaultEndpoint        - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=AUTH, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.AsyncCommand]
2020-07-04 19:43:45.756 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] write(ctx, AsyncCommand [type=AUTH, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.AsyncCommand], promise)
2020-07-04 19:43:45.759 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandEncoder         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379] writing command AsyncCommand [type=AUTH, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.AsyncCommand]
2020-07-04 19:43:45.759 14548 --- [ioEventLoop-4-1] TRACE i.l.core.protocol.CommandEncoder         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379] Sent: *2
$4
AUTH
$6
password
2020-07-04 19:43:45.762 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.DefaultEndpoint        - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, epid=0x1] write() done
2020-07-04 19:43:45.762 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.ConnectionWatchdog     - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, last known addr=www.yorozuyas.com/121.36.211.56:6379] userEventTriggered(ctx, io.lettuce.core.ConnectionEvents$Activated@8ff7c65)
2020-07-04 19:43:45.762 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.ConnectionWatchdog     - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, last known addr=www.yorozuyas.com/121.36.211.56:6379] userEventTriggered(ctx, io.lettuce.core.ConnectionEvents$Activated@8ff7c65)
2020-07-04 19:43:45.769 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Received: 5 bytes, 1 commands in the stack
2020-07-04 19:43:45.769 14548 --- [ioEventLoop-4-1] TRACE i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Buffer: +OK
2020-07-04 19:43:45.769 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Stack contains: 1 commands
2020-07-04 19:43:45.769 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.RedisStateMachine      - Decode AsyncCommand [type=AUTH, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.AsyncCommand]
2020-07-04 19:43:45.772 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.RedisStateMachine      - Decoded AsyncCommand [type=AUTH, output=StatusOutput [output=OK, error='null'], commandType=io.lettuce.core.protocol.AsyncCommand], empty stack: true
2020-07-04 19:43:45.774 14548 --- [nio-8080-exec-2] DEBUG io.lettuce.core.RedisChannelHandler      - dispatching command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 19:43:45.774 14548 --- [nio-8080-exec-2] DEBUG i.l.core.protocol.DefaultEndpoint        - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 19:43:45.775 14548 --- [nio-8080-exec-2] DEBUG i.l.core.protocol.DefaultEndpoint        - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, epid=0x1] write() done
2020-07-04 19:43:45.775 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] write(ctx, AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], promise)
2020-07-04 19:43:45.776 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandEncoder         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379] writing command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 19:43:45.776 14548 --- [ioEventLoop-4-1] TRACE i.l.core.protocol.CommandEncoder         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379] Sent: *2
$3
GET
$16
address:zhangsan
2020-07-04 19:43:45.786 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Received: 5 bytes, 1 commands in the stack
2020-07-04 19:43:45.786 14548 --- [ioEventLoop-4-1] TRACE i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Buffer: $-1
2020-07-04 19:43:45.786 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Stack contains: 1 commands
2020-07-04 19:43:45.786 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.RedisStateMachine      - Decode AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 19:43:45.786 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.RedisStateMachine      - Decoded AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], empty stack: true
2020-07-04 19:43:45.817 14548 --- [nio-8080-exec-2] DEBUG c.y.d.d.P.fetchAddress                   - ==>  Preparing: SELECT id, uid, name, address, mobile FROM tb_deliver_address WHERE uid = ? 
2020-07-04 19:43:45.834 14548 --- [nio-8080-exec-2] DEBUG c.y.d.d.P.fetchAddress                   - ==> Parameters: zhangsan(String)
2020-07-04 19:43:45.857 14548 --- [nio-8080-exec-2] DEBUG c.y.d.d.P.fetchAddress                   - <==      Total: 4
2020-07-04 19:43:45.875 14548 --- [nio-8080-exec-2] DEBUG io.lettuce.core.RedisChannelHandler      - dispatching command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 19:43:45.876 14548 --- [nio-8080-exec-2] DEBUG i.l.core.protocol.DefaultEndpoint        - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 19:43:45.876 14548 --- [nio-8080-exec-2] DEBUG i.l.core.protocol.DefaultEndpoint        - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, epid=0x1] write() done
2020-07-04 19:43:45.876 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] write(ctx, AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], promise)
2020-07-04 19:43:45.877 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandEncoder         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379] writing command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 19:43:45.877 14548 --- [ioEventLoop-4-1] TRACE i.l.core.protocol.CommandEncoder         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379] Sent: *5
$3
SET
$16
address:zhangsan
$538
?	ArrayList?'com.yorozuyas.demo.model.DeliverAddresszhangsan鎴戠殑濮愬"-婀栧寳鐪佹姹夊競姹熸眽鍖哄彂灞曞ぇ閬?185鍙?(靖瘙-?'com.yorozuyas.demo.model.DeliverAddresszhangsan鎴戠殑濡瑰"6瀹夊窘鐪佸悎鑲ュ競缁忔祹鎶?鏈紑鍙戝尯鑺欒搲璺?678鍙?(扛瘙-?'com.yorozuyas.demo.model.DeliverAddresszhangsan鎴戠殑寮熷紵"9娴欐睙鐪佹澀宸炲競婊ㄦ睙鍖烘睙鍗楀ぇ閬撻緳婀栨花姹熷ぉ琛?(栏瘙-?'com.yorozuyas.demo.model.DeliverAddresszhangsan寮犱笁">骞夸笢鐪佷經灞卞競椤哄痉鍖哄ぇ鑹繋瀹捐矾纰ф鍥烽捇鐭虫咕(粮瘙-
$2
PX
$7
1800000

从控制台可以看出,在请求过来之后,SpringCache注解起作用了,首先是执行登录请求,然后查询address:zhangsan,结果返回 Buffer: $-1,说明缓存中没有这条信息,那么就执行数据库查询操作,最后把查询结果添加到缓存中。

  • 查看redis监控
    SpringBoot整合Redis + SpringCache + Protobuf,优雅地实现key-value键值对存储DEMO。_第4张图片
    可以看到先有一个登录请求过来,然后执行了GET查询操作,最后执行了SET添加操作,和上面分析一致。

  • 再调用一次查询收货地址api
    SpringBoot整合Redis + SpringCache + Protobuf,优雅地实现key-value键值对存储DEMO。_第5张图片

  • 查看控制台输出

2020-07-04 20:13:36.682 14548 --- [nio-8080-exec-5] INFO  c.y.d.c.Protobuf2RedisController         - Request-Method: GET, Request-Path: /fetch, uid: zhangsan
2020-07-04 20:13:36.705 14548 --- [nio-8080-exec-5] DEBUG io.lettuce.core.RedisChannelHandler      - dispatching command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 20:13:36.705 14548 --- [nio-8080-exec-5] DEBUG i.l.core.protocol.DefaultEndpoint        - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 20:13:36.706 14548 --- [nio-8080-exec-5] DEBUG i.l.core.protocol.DefaultEndpoint        - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, epid=0x1] write() done
2020-07-04 20:13:36.706 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] write(ctx, AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], promise)
2020-07-04 20:13:36.706 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandEncoder         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379] writing command AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 20:13:36.707 14548 --- [ioEventLoop-4-1] TRACE i.l.core.protocol.CommandEncoder         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379] Sent: *2
$3
GET
$16
address:zhangsan
2020-07-04 20:13:36.715 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Received: 512 bytes, 1 commands in the stack
2020-07-04 20:13:36.715 14548 --- [ioEventLoop-4-1] TRACE i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Buffer: $538
?	ArrayList?'com.yorozuyas.demo.model.DeliverAddresszhangsan鎴戠殑濮愬"-婀栧寳鐪佹姹夊競姹熸眽鍖哄彂灞曞ぇ閬?185鍙?(靖瘙-?'com.yorozuyas.demo.model.DeliverAddresszhangsan鎴戠殑濡瑰"6瀹夊窘鐪佸悎鑲ュ競缁忔祹鎶?鏈紑鍙戝尯鑺欒搲璺?678鍙?(扛瘙-?'com.yorozuyas.demo.model.DeliverAddresszhangsan鎴戠殑寮熷紵"9娴欐睙鐪佹澀宸炲競婊ㄦ睙鍖烘睙鍗楀ぇ閬撻緳婀栨花姹熷ぉ琛?(栏瘙-?'com.yorozuyas.demo.model.DeliverAddresszhangsan寮犱笁">骞夸笢鐪佷經灞卞競椤哄痉鍖哄ぇ鑹繋瀹
2020-07-04 20:13:36.715 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Stack contains: 1 commands
2020-07-04 20:13:36.715 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.RedisStateMachine      - Decode AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 20:13:36.715 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.RedisStateMachine      - Decoded AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command], empty stack: false
2020-07-04 20:13:36.716 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Received: 34 bytes, 1 commands in the stack
2020-07-04 20:13:36.716 14548 --- [ioEventLoop-4-1] TRACE i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Buffer: 捐矾纰ф鍥烽捇鐭虫咕(粮瘙-
2020-07-04 20:13:36.716 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.CommandHandler         - [channel=0xc4d24ef3, /192.168.3.16:50627 -> www.yorozuyas.com/121.36.211.56:6379, chid=0x1] Stack contains: 1 commands
2020-07-04 20:13:36.716 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.RedisStateMachine      - Decode AsyncCommand [type=GET, output=ValueOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
2020-07-04 20:13:36.716 14548 --- [ioEventLoop-4-1] DEBUG i.l.core.protocol.RedisStateMachine      - Decoded AsyncCommand [type=GET, output=ValueOutput [output=[B@2e463c2d, error='null'], commandType=io.lettuce.core.protocol.Command], empty stack: true

可以看到,执行了查询Redis,并得到结果Buffer: $538,然后就返回数据,没有执行数据库操作(没有打印SQL信息判断得知)。
另外,redis缓存的数据分别打印了两次,出现了Netty的读写半包,有兴趣的同学可以学习一下Netty,可参考《Netty权威指南》或《Netty实战》。

  • 查看redis监控
    在这里插入图片描述
    监控得知,执行了查询操作,后面就没有其他输出信息了,证明查到数据了。
  • 其他接口就不再测试了,可自行进行测试。

结束语

  • 总体来说Spring对Redis的集成度很高,在没有特殊需求的情况下,引入类库就可直接使用Spring注解进行缓存集成。而其他缓存如Memcached就没有得到Spring的这般对待了,要想集成Spring注解,需要自己实现CacheManager和Cache接口。
  • 文中提供的org.springframework.data.redis.cache.RedisCacheConfiguration,针对的是全局的,也就是默认的。redis可以针对不同的命名空间提供不同的配置,具体需要自定义一个RedisCacheManager Bean,并执行RedisCacheManagerBuilder方法withInitialCacheConfigurations添加Map

你可能感兴趣的:(spring,boot,redis)