一、本文要點
接上文,我們已經把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);
需要源碼的同學私聊
加我一起交流學習!
