一、本文要点
接上文,我们已经把SpringBoot整合mybatis+Hikari+es了,并且通过docker搭建好了redis环境,本文将介绍SpringBoot如何整合redis,利用缓存技术,使接口快得飞起来。系列文章完整目录
-
redis操作工具类
-
lettuce连接池
-
cacheManager注解使用,自动缓存和失效移除、序列化器
-
springboot整合redis,lettuce,redis集群
-
单元测试回滚数据库事务,junit5重复测试,assertThat
-
springboot + mybatis + Hikari + elasticsearch + redis
二、开发环境
- jdk 1.8
- maven 3.6.2
- mybatis 1.3.0
- springboot 2.4.3
- mysql 5.6.46
- junit 5
- redis4.0
- idea 2020
三、修改pom.xml 增加依赖
这里使用lettuce,如果您使用jedis,需要增加对应的依赖。
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 要用redis连接池 必须有pool依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.5.0</version>
</dependency>
三、修改配置文件
修改application-dev.properties文件,同理,后面发布到测试、正式环境的话,修改对应的配置文件。这里使用lettuce作为连接池,如果您使用jedis,需要对应修改一下名称。
#################### REDIS ####################
# 单机版
spring.redis.host=9.134.xxx.xxx
# 集群版
# spring.redis.cluster.nodes=9.134.xxx.xxx:6379
spring.redis.password=9uWNx7uJJtA/wkQ43534dXcURyVpWfiZ/a
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-wait=3000
spring.redis.lettuce.pool.min-idle=4
spring.redis.timeout=2s
redis.expired=300
五、增加配置类
1、增加RedisConfig.java,接收application-dev.peoperties的配置。
@Component("redis")
@ConfigurationProperties(prefix = "redis")
@Data
public class RedisConfig {
/**
* 默认过期时间.
*/
private int expired;
}
2、编写RedisConfiguration.java,配置redisTemplate和cacheManager,key使用StringRedisSerializer序列化器,value使用Jackson2JsonRedisSerializer序列化器,还重写了默认的keyGenerator。
@Configuration
@EnableCaching
public class RedisConfiguration extends CachingConfigurerSupport {
@Resource
private RedisConfig redisConfig;
@Bean("redisKeyPrefix")
public String redisKeyPrefix() {
return MMC_MEMBER_KEY_PREFIX;
}
@Bean("redisTemplate")
public RedisTemplate<?, ?> getRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<?, ?> template = new StringRedisTemplate(connectionFactory);
StringRedisSerializer keySerializer = new StringRedisSerializer();
template.setKeySerializer(keySerializer);
// Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// ObjectMapper om = new ObjectMapper();
// om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// jackson2JsonRedisSerializer.setObjectMapper(om);
// template.setValueSerializer(new JdkSerializationRedisSerializer());
template.setValueSerializer(createJacksonRedisSerializer());
template.setHashKeySerializer(keySerializer);
template.setHashValueSerializer(createJacksonRedisSerializer());
return template;
}
// 缓存管理器
@Bean("memberCacheManager")
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 生成一个默认配置,通过config对象即可对缓存进行自定义配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(redisConfig.getExpired())) // 设置缓存的默认过期时间,也是使用Duration设置
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(createJacksonRedisSerializer()))
.disableCachingNullValues(); // 不缓存空值
// 对每个缓存空间应用不同的配置
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
redisCacheConfigurationMap.put(redisKeyPrefix(), config);
// 初始化一个RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
// 初始化RedisCacheManager
return new RedisCacheManager(redisCacheWriter, config, redisCacheConfigurationMap);
/*
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) // 使用自定义的缓存配置初始化一个cacheManager
.initialCacheNames(cacheNames) // 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
.withInitialCacheConfigurations(configMap)
.build();
*/
}
private Jackson2JsonRedisSerializer<?> createJacksonRedisSerializer() {
Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
}
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
String[] value = new String[1];
Cacheable cacheable = method.getAnnotation(Cacheable.class);
if (cacheable != null) {
value = cacheable.value();
}
CachePut cachePut = method.getAnnotation(CachePut.class);
if (cachePut != null) {
value = cachePut.value();
}
CacheEvict cacheEvict = method.getAnnotation(CacheEvict.class);
if (cacheEvict != null) {
value = cacheEvict.value();
}
sb.append(value[0]);
for (Object obj : params) {
sb.append(":")
.append(obj.toString());
}
return sb.toString();
};
}
3、修改MemberService.java,使用cacheManager注解自动管理缓存.
@Slf4j
@Service
public class MemberService {
@Resource
private TblMemberInfoMapper tblMemberInfoMapper;
@Resource
private ElasticSearchConfig elasticSearchConfig;
@Resource
private RestHighLevelClient restHighLevelClient;
@Resource(name = "esObjectMapper")
private ObjectMapper objectMapper;
// 但数据有更新的时候使缓存失效,这里是为了举例,缓存数据一致性问题先不考虑
@CacheEvict(key = "#member.uid", cacheNames = {Const.MMC_MEMBER_KEY_PREFIX})
public TblMemberInfo save(TblMemberInfo member) {
tblMemberInfoMapper.upsert(member);
return member;
}
// 增加缓存,常量值为mmc:member,拼接后的key为mmc:member:id
@Cacheable(key = "#member.uid", cacheNames = {Const.MMC_MEMBER_KEY_PREFIX})
public TblMemberInfo get(TblMemberInfo member) {
log.info("!!!!!!!!!!!!!!!!!!!!!!!!!!!! load data from db. !!!!!!!!!!!!!!!!!!!!!!!!!!!!");
return tblMemberInfoMapper.selectByPrimaryKey(member.getUid());
}
}
六、运行一下
1、编写单元测试。
@Slf4j
@ActiveProfiles("dev")
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional // 自动回滚单元测试插入DB的数据
public class MemberServiceTest {
@Resource
private MemberService memberService;
@Resource(name = "esObjectMapper")
private ObjectMapper objectMapper;
/**
* add.
*/
@Rollback(false) // 为了测试redis,暂时不回滚数据
@Test
public void testAdd() {
TblMemberInfo member = new TblMemberInfo();
member.setUid(8888L);
member.setUname("zhangsan");
member.setUsex(1);
member.setUbirth(new Date());
member.setUtel("888");
member.setUaddr("凌霄殿");
member.setState(0);
member.setDelFlag(0);
member.setUphoto(null);
TblMemberInfo ret = memberService.save(member);
assertThat(ret).isNotNull();
log.info("--------------------------------------------------");
log.info(ret.getUname());
}
/**
* get.
*/
@RepeatedTest(5) // 重复5次,可以看到只打印一次 load data from db
void get() {
TblMemberInfo member = new TblMemberInfo();
member.setUid(8888L);
TblMemberInfo ret = memberService.get(member);
assertThat(ret).isNotNull();
log.info("--------------------------------------------------");
log.info(ret.getUname());
assertThat(ret.getUname()).isEqualTo("zhangsan");
}
}
2、效果,可以看到load data from db 只打印一次,并且数据已经存入redis。
[2021-03-15 15:04:01.419] [main] [INFO] [com.mmc.lesson.member.service.MemberService:?] - !!!!!!!!!!!!!!!!!!!!!!!!!!!! load data from db. !!!!!!!!!!!!!!!!!!!!!!!!!!!!
[2021-03-15 15:04:01.421] [main] [DEBUG] [c.m.l.m.m.TblMemberInfoMapper.selectByPrimaryKey:?] - ==> Preparing: select uid, uname, usex, ubirth, utel, uaddr, createTime, updateTime, state, delFlag , uphoto from Tbl_MemberInfo where uid = ?
[2021-03-15 15:04:01.422] [main] [DEBUG] [c.m.l.m.m.TblMemberInfoMapper.selectByPrimaryKey:?] - ==> Parameters: 8888(Long)
[2021-03-15 15:04:01.455] [main] [DEBUG] [io.lettuce.core.RedisChannelHandler:?] - dispatching command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
[2021-03-15 15:04:01.455] [main] [DEBUG] [io.lettuce.core.protocol.DefaultEndpoint:?] - [channel=0xc023648b, /192.168.xxx.xxx:58618 -> /9.134.xxx.xxx:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=SET, output=StatusOutput [output=null, error='null'], commandType=io.lettuce.core.protocol.Command]
[2021-03-15 15:04:01.463] [lettuce-nioEventLoop-4-1] [DEBUG] [io.lettuce.core.protocol.RedisStateMachine:?] - Decode done, empty stack: true
[2021-03-15 15:04:01.463] [main] [INFO] [com.mmc.lesson.member.service.MemberServiceTest:?] - zhangsan
9.134.xxx.xxx:6379> get mmc:member::8888
"[\"com.mmc.lesson.member.model.TblMemberInfo\",{\"uid\":8888,\"uname\":\"zhangsan\",\"usex\":1,\"ubirth\":[\"java.util.Date\",1615791840000],\"utel\":\"888\",\"uaddr\":\"\xe5\x87\x8c\xe9\x9c\x84\xe6\xae\xbf\",\"createTime\":null,\"updateTime\":null,\"state\":0,\"delFlag\":0,\"uphoto\":null}]"
9.134.xxx.xxx:6379>
七、小结
这里只是简单介绍如何整合redis,更加详细的用法请关注后续文章,完整代码地址:戳这里。下一篇《搭建大型分布式服务(十二)Docker搭建开发环境安装kafka》
有同学问,如果不用cacheManager,怎样操作redis呢,我们可以使用redisTemplate,源码里封装了常用的工具类DoeRedisResolver.java去操作redis方法,有兴趣的同学可以阅读以下。
1、使用方法。
@Slf4j
@ActiveProfiles("dev")
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class DoeRedisResolverTest {
@Resource
private RedisResolver redisResolver;
@Test
void testAdd() {
String key = "mmc:member:1";
TblMemberInfo member = new TblMemberInfo();
member.setUid(8888L);
member.setUname("zhangsan");
member.setUsex(1);
member.setUbirth(new Date());
member.setUtel("888");
member.setUaddr("凌霄殿");
member.setState(0);
member.setDelFlag(0);
member.setUphoto(null);
redisResolver.set(key, member, 30);
TblMemberInfo data = (TblMemberInfo) redisResolver.get(key);
assertThat(data.getUname()).isEqualTo("zhangsan");
}
}
2、接口概览,来源网络外加了自己的封装。
public interface RedisResolver {
/**
* 获取模板.
*
* @return
*/
RedisTemplate<String, Object> getRedisTemplate();
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
boolean expire(String key, long time);
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
long getExpire(String key);
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
boolean hasKey(String key);
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
void del(String... key);
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
Object get(String key);
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
boolean set(String key, Object value);
/**
* 普通缓存放入并设置时间.
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
boolean set(String key, Object value, long time);
/**
* 普通缓存放入并设置时间.
* @param key
* @param value
* @param time
* @param unit
* @return
*/
boolean set(String key, Object value, long time, TimeUnit unit);
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
long incr(String key, long delta);
/**
* 递减
* @param key 键
* @param delta 要减少几(大于0)
* @return
*/
long decr(String key, long delta);
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
Object hget(String key, String item);
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
Map<Object, Object> hmget(String key);
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
boolean hmset(String key, Map<String, Object> map);
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
boolean hmset(String key, Map<String, Object> map, long time);
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
boolean hset(String key, String item, Object value);
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
boolean hset(String key, String item, Object value, long time);
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
void hdel(String key, Object... item);
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
boolean hHasKey(String key, String item);
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
double hincr(String key, String item, double by);
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
double hdecr(String key, String item, double by);
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
Set<Object> sMembers(String key);
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
boolean sHasKey(String key, Object value);
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
long sAdd(String key, Object... values);
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
long sSetAndTime(String key, long time, Object... values);
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
long sGetSetSize(String key);
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
long sRem(String key, Object... values);
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
List<Object> lGet(String key, long start, long end);
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
long lGetListSize(String key);
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
Object lGetIndex(String key, long index);
/**
* 将list放入缓存.
* @param key
* @param value
* @return
*/
boolean lSet(String key, Object value);
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
boolean lSet(String key, Object value, long time);
/**
* 将list放入缓存.
* @param key
* @param value
* @return
*/
boolean lSet(String key, List<Object> value);
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
boolean lSet(String key, List<Object> value, long time);
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
boolean lUpdateIndex(String key, long index, Object value);
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
long lRemove(String key, long count, Object value);
/**
* 从右边加入队列.
* @param key
* @param value
* @return
*/
boolean rPush(String key, Object value);
/**
* 从左边出队.
* @param key
* @return
*/
Object lPop(String key);
需要源码的同学私聊
加我一起交流学习!