Redis 本身的一些概念
Redis 支持的數(shù)據(jù)結(jié)構(gòu)
- String 字符串
- Hash 字典
- List 列表
- Set 集合
- Sorted Set 有序集合
String 和 Hash 的對(duì)比
String 實(shí)際是就是一個(gè) Key - Value 的映射;
Hash 就是一個(gè) Key - (Key - Value) 的兩層映射。
# redis-cli
# Redis 中命令不區(qū)分大小寫(xiě)。這里命令使用小寫(xiě),僅在特別的地方用大寫(xiě)。
# 參數(shù)使用“大寫(xiě)+下劃線”的方式。
# String
set KEY VALUE
get KEY
# Hash
hset HASH_NAME KEY VALUE
hget HASH_NAME KEY
hMset HASH_NAME KEY0 VALUE0 KEY1 VALUE1 ...
hMget HASH_NAME KEY0 KEY1 ...
STACK OVERFLOW 上一個(gè)對(duì) String 和 Hash 的討論
對(duì)于一個(gè)對(duì)象是把本身的數(shù)據(jù)序列化后用 String 存儲(chǔ),還是使用 Hash 來(lái)分別存儲(chǔ)對(duì)象的各個(gè)屬性:
- 如果在大多數(shù)時(shí)候要訪問(wèn)對(duì)象的大部分?jǐn)?shù)據(jù):使用 String
- 如果在大多數(shù)時(shí)候只要訪問(wèn)對(duì)象的小部分?jǐn)?shù)據(jù):使用 Hash
- 如果對(duì)象里面還有對(duì)象這種結(jié)構(gòu)復(fù)雜的,最好用 String。否則最外層用 Hash,里面又將對(duì)象序列化,兩者混用可能導(dǎo)致混亂。
Spring Boot 添加 Redis 的配置
以 gradle 為例。
- 修改
build.gradle
compile("org.springframework.boot:spring-boot-starter-data-redis")
- 修改
application.yml
spring:
# redis
redis:
host: 127.0.0.1
# 數(shù)據(jù)庫(kù)索引(默認(rèn)為0)
database: 0
port: 6379
password: PASSWORD
# 連接池中的最大空閑連接
pool.max-idle: 8
# 連接池中的最小空閑連接
pool.min-idle: 0
# 連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制)
pool.max-active: 8
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)
pool.max-wait: -1
# 連接超時(shí)時(shí)間(毫秒)
timeout: 0
- 添加
RedisConfig
package zz.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* RedisConfig
*
* @author zz
* @date 2018/5/7
*/
@Configuration
@EnableCaching
@Slf4j
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public KeyGenerator wiselyKeyGenerator() {
return new KeyGenerator() {
private static final String SEPARATE = ":";
@Override
public Object generate(Object target, Method method, Object... params) {
log.debug("+++++generate");
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(SEPARATE).append(method);
for (Object obj : params) {
sb.append(SEPARATE).append(obj);
}
return sb.toString();
}
};
}
/**
* http://www.itdecent.cn/p/9255b2484818
*
* TODO: 對(duì) Spring @CacheXXX 注解進(jìn)行擴(kuò)展:注解失效時(shí)間 + 主動(dòng)刷新緩存
*/
@Bean
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
log.debug("++++cacheManager");
RedisCacheManager redisCacheManager =new RedisCacheManager(redisTemplate);
redisCacheManager.setTransactionAware(true);
redisCacheManager.setLoadRemoteCachesOnStartup(true);
// 最終在 Redis 中的 key = @Cacheable 注解中 'cacheNames' + 'key'
redisCacheManager.setUsePrefix(true);
// 所有 key 的默認(rèn)過(guò)期時(shí)間,不設(shè)置則永不過(guò)期
// redisCacheManager.setDefaultExpiration(6000L);
// 對(duì)某些 key 單獨(dú)設(shè)置過(guò)期時(shí)間
// 這里的 key 是 @Cacheable 注解中的 'cacheNames'
Map<String, Long> expires = new HashMap<>(10);
// expires.put("feedCategoryDto", 5000L);
// expires.put("feedDto", 5000L);
redisCacheManager.setExpires(expires);
return redisCacheManager;
}
// value serializer
private Jackson2JsonRedisSerializer getJackson2JsonRedisSerializer() {
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;
}
private GenericJackson2JsonRedisSerializer getGenericJackson2JsonRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
/**
*
* Once configured, the template is thread-safe and can be reused across multiple instances.
* -- https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/
*/
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
log.debug("++++redisTemplate");
StringRedisTemplate template = new StringRedisTemplate(factory);
// key serializer
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisSerializer valueRedisSerializer;
// -- 1 Jackson2JsonRedisSerializer
// valueRedisSerializer = getJackson2JsonRedisSerializer();
// -- 2 GenericJackson2JsonRedisSerializer
valueRedisSerializer = getGenericJackson2JsonRedisSerializer();
// set serializer
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(valueRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(valueRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
RedisConfig 中定義了三個(gè)函數(shù),主要作用如下:
- wiselyKeyGenerator:定義了一個(gè)生成 Redis 的 key 的方法。如下文使用了 @Cacheable 注解的地方,可以指定 key 的生成方法使用我們這個(gè)函數(shù)。
- cacheManager:定義了對(duì) Redis 的一些基本設(shè)置。
- redisTemplate:對(duì)我們要使用的 RedisTemplate 做一些設(shè)置。主要是確定序列化方法。
RedisTemplate 設(shè)置序列化器
Spring Redis 雖然提供了對(duì) list、set、hash 等數(shù)據(jù)類型的支持,但是沒(méi)有提供對(duì) POJO 對(duì)象的支持,底層都是把對(duì)象序列化后再以字節(jié)的方式存儲(chǔ)的。
因此,Spring Data Redis 提供了若干個(gè) Serializer,主要包括:
- JdkSerializationRedisSerializer: 默認(rèn)的序列化器。序列化速度快,生成的字節(jié)長(zhǎng)度較大。
- OxmSerializer: 生成 XML 格式的字節(jié)。
- StringSerializer: 只能對(duì) String 類型進(jìn)行序列化。
- JacksonJsonRedisSerializer:以 JSON 格式進(jìn)行序列化。
- Jackson2JsonRedisSerializer:JacksonJsonRedisSerializer 的升級(jí)版。
- GenericJackson2JsonRedisSerializer:Jackson2JsonRedisSerializer 的泛型版。
RedisTemplate 中需要聲明 4 種 serializer(默認(rèn)使用的是 JdkSerializationRedisSerializer):
- keySerializer :對(duì)于普通 K-V 操作時(shí),key 采取的序列化策略
- valueSerializer:value 采取的序列化策略
- hashKeySerializer: 在 hash 數(shù)據(jù)結(jié)構(gòu)中,hash-key 的序列化策略
- hashValueSerializer:hash-value 的序列化策略
無(wú)論如何,建議 key/hashKey 采用 StringRedisSerializer。
我們?cè)O(shè)置了 serializer 后,讀寫(xiě) Redis 要使用同一種 serizlizer,否則會(huì)讀不出之前用不同 serializer 寫(xiě)入的數(shù)據(jù)。
也就是設(shè)置 valueSerializer 為GenericJackson2JsonRedisSerializer,然后寫(xiě)入了數(shù)據(jù)。
后面要讀數(shù)據(jù)的時(shí)候,如果將 valueSerializer 又設(shè)置成了 Jackson2JsonRedisSerializer,那么讀取數(shù)據(jù)時(shí)就會(huì)報(bào)錯(cuò)。
通常情況下,我們只需要在 RedisConfig 中統(tǒng)一設(shè)置好 4 個(gè) serializer 即可。
Jackson2JsonRedisSerializer 與 GenericJackson2JsonRedisSerializer 的對(duì)比
- 兩者都是將對(duì)象的數(shù)據(jù)序列化成 JSON 格式的字符串。
- Jackson2JsonRedisSerializer 需要自己指定 ObjectMaper 或某個(gè)特定的類型。
- GenericJackson2JsonRedisSerializer 是 Jackson2JsonRedisSerializer 的一個(gè)特例,默認(rèn)支持所有類型。
- 兩者序列化時(shí),都會(huì)將原始對(duì)象的類名和包名寫(xiě)入 JSON 字符串中。以便反序列化時(shí),確認(rèn)要將 JSON 轉(zhuǎn)成何種格式。
可用如下方式來(lái)獲得通用的 Jackson2JsonRedisSerializer
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);
Jackson2JsonRedisSerializer 與 GenericJackson2JsonRedisSerializer 生成 JSON 的對(duì)比
# Jackson2JsonRedisSerializer 序列化的效果
127.0.0.1:6379> get 123
"[\"zz.domain.User\",{\"id\":123,\"name\":\"name\"}]"
127.0.0.1:6379> get userList
"[\"java.util.LinkedList\",[[\"zz.domain.User\",{\"id\":233,\"name\":\"new\"}],[\"zz.domain.User\",{\"id\":233,\"name\":\"new\"}]]]"
# GenericJackson2JsonRedisSerializer 序列化的效果
127.0.0.1:6379> get 123
"{\"@class\":\"zz.domain.User\",\"id\":123,\"name\":\"name\"}"
127.0.0.1:6379> get userList
"[\"java.util.LinkedList\",[{\"@class\":\"zz.domain.User\",\"id\":233,\"name\":\"new\"},{\"@class\":\"zz.domain.User\",\"id\":233,\"name\":\"new\"}]]"
如何使用
使用注解來(lái)緩存函數(shù)的結(jié)果
在要緩存的方法上使用注解 @Cacheable、@CachePut、@CacheEvict 分別用于緩存返回?cái)?shù)據(jù)、更新緩存數(shù)據(jù)、刪除緩存數(shù)據(jù)。
package zz.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import zz.domain.User;
/**
* UserService
*
* @author zz
* @date 2018/5/7
*/
@Service
@Slf4j
public class UserService {
public final String DEFAULT_NAME = "def";
@Cacheable(cacheNames = "user", key = "'id_'+#userId")
public User get(int userId) {
// get from db
log.debug("[++] get userId=" + userId);
User user = new User();
user.setId(userId);
user.setName(DEFAULT_NAME);
log.debug("[++] create default user=" + user);
return user;
}
@CachePut(cacheNames = "user", key = "'id_'+#user.getId()")
public User update(User user) {
// save to db
log.debug("[++] update user=" + user);
return user;
}
@CacheEvict(cacheNames = "user", key = "'id_'+#userId")
public void delete(int userId) {
// delete from db
log.debug("[++] delete userId=" + userId);
}
@CachePut(cacheNames = "user", key = "'id_'+#userId")
public User updateName(int userId, String name) {
// update to db
log.debug("[++] updateName userId=" + userId + ", name=" + name);
User user = get(userId);
user.setName(name);
return user;
}
public void innerCall(int userId) {
log.debug("[++] innerCall");
get(userId);
}
}
- 對(duì)函數(shù)的緩存是通過(guò)代理來(lái)實(shí)現(xiàn)的 :
類內(nèi)部的某個(gè)函數(shù)對(duì)其他函數(shù)(即便被調(diào)用函數(shù)有@CacheXXX注解)的調(diào)用是不會(huì)走代理的,也就沒(méi)有緩存。(比如innerCall調(diào)用get時(shí)不會(huì)使用緩存) 。 - 注解可以放到 Service、Dao 或 Controller 層。
-
@CacheXXX會(huì)緩存函數(shù)的返回值。比如increaseComment會(huì)緩存更新后的FeedCount。 - 當(dāng)緩存中有數(shù)據(jù)時(shí),
@Cacheable注解的函數(shù)不會(huì)執(zhí)行,直接返回緩存中的數(shù)據(jù)。 -
@CachePut、@CacheEvit注解的函數(shù),無(wú)論如何都會(huì)執(zhí)行。
自定義緩存
如果要更細(xì)粒度地控制 Redis,可以使用 RedisTemplate、StringRedisTemplate
StringRedisTemplate 是 RedisTemplate 的一個(gè)特例:key 和 value 都是 String 類型。
- RedisTemplate 默認(rèn)使用 JDK 對(duì) key 和 value 進(jìn)行序列化,轉(zhuǎn)成字節(jié)存入 Redis。
- StringRedisTemplate 的 key、value 本身就是 String,使用 StringRedisSerializer 將 String 轉(zhuǎn)成字節(jié)存入 Redis。
當(dāng)我們將 RedisTemplate 的 keySerializer 和 valueSerializer 都設(shè)置成了 StringRedisSerializer,則 RedisTemplate 和 StringRedisTemplate 的效果是相同的,就像下面的樣例所示。
RedisTemplate 對(duì) Redis 中各個(gè)數(shù)據(jù)結(jié)構(gòu)的操作
- redisTemplate.opsForValue();//操作字符串
- redisTemplate.opsForHash();//操作hash
- redisTemplate.opsForList();//操作list
- redisTemplate.opsForSet();//操作set
- redisTemplate.opsForZSet();//操作有序set
package zz;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
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.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.test.context.junit4.SpringRunner;
import zz.domain.User;
import zz.service.UserService;
import java.util.LinkedList;
import java.util.List;
/**
* zz.TestRedis
*
* @author zz
* @date 2018/5/7
*/
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
public class TestRedis {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate redisTemplate;
@Autowired
UserService userService;
@Test
public void testSerializer() {
// 1.
// 這里的 opsForValue().get() 的參數(shù)必須轉(zhuǎn)成 String 類型。
// 除非在 RedisConfig 中 將 keySerializer 設(shè)置成 GenericJackson2JsonRedisSerializer 等能將其他類型轉(zhuǎn)換成 String 的。
// 2.
// 如果切換了 RedisConfig 中的 ValueSerializer,要先用 redis-cli 將其中的舊數(shù)據(jù)刪除。
// 不同 Serializer 格式之間的轉(zhuǎn)換可能存在問(wèn)題。
final int ID = 123;
User oldUser;
oldUser = (User) redisTemplate.opsForValue().get(String.valueOf(ID));
log.debug("oldUser=" + oldUser);
User user = new User();
user.setId(ID);
user.setName("name");
log.debug("user=" + user);
redisTemplate.opsForValue().set(String.valueOf(user.getId()), user);
User newUser;
newUser = (User) redisTemplate.opsForValue().get(String.valueOf(ID));
log.debug("newUser=" + newUser);
Assert.assertEquals(user.getId(), newUser.getId());
Assert.assertEquals(user.getName(), newUser.getName());
List<User> userList = new LinkedList<>();
userList.add(user);
user.setId(233);
user.setName("new");
userList.add(user);
redisTemplate.opsForValue().set("userList", userList);
List<User> newUserList;
newUserList = (List<User>) redisTemplate.opsForValue().get("userList");
Assert.assertEquals(userList, newUserList);
}
@Test
public void testSerizlizer2() {
// 保存用于恢復(fù),以免影響其他部分
RedisSerializer oldKeySerializer = redisTemplate.getKeySerializer();
RedisSerializer oldValueSerializer = redisTemplate.getValueSerializer();
RedisSerializer redisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setValueSerializer(redisSerializer);
final String KEY = "key";
String VALUE = "value";
redisTemplate.opsForValue().set(KEY, VALUE);
Assert.assertEquals(VALUE, redisTemplate.opsForValue().get(KEY));
Assert.assertEquals(VALUE, stringRedisTemplate.opsForValue().get(KEY));
VALUE = "Val2";
stringRedisTemplate.opsForValue().set(KEY, VALUE);
Assert.assertEquals(VALUE, stringRedisTemplate.opsForValue().get(KEY));
Assert.assertEquals(VALUE, redisTemplate.opsForValue().get(KEY));
// 恢復(fù)原本設(shè)置
redisTemplate.setKeySerializer(oldKeySerializer);
redisTemplate.setValueSerializer(oldValueSerializer);
}
@Test
public void testCache() {
final int USER_ID = 1;
User user = userService.get(USER_ID);
log.debug("user=" + user);
Assert.assertEquals(userService.DEFAULT_NAME, user.getName());
// 這次會(huì)直接返回 cache
user = userService.get(USER_ID);
log.debug("user=" + user);
// 獲得修改過(guò)的 cache
final String ANOTHER_NAME = "another user";
user.setName(ANOTHER_NAME);
userService.update(user);
user = userService.get(USER_ID);
log.debug("user=" + user);
Assert.assertEquals(ANOTHER_NAME, user.getName());
// 直接調(diào)用 get 會(huì)走緩存,通過(guò) innerCall 來(lái)調(diào)用 get 不會(huì)走緩存
log.debug("------ before");
userService.get(USER_ID);
log.debug("------ middle");
userService.innerCall(USER_ID);
log.debug("------ after");
// 另一種修改的方式
final String NEW_NAME = "updated";
userService.updateName(USER_ID, NEW_NAME);
user = userService.get(USER_ID);
log.debug("user=" + user);
Assert.assertEquals(NEW_NAME, user.getName());
// 刪除后,cache 中的數(shù)據(jù)會(huì)被刪除,name 會(huì)變成初始值
userService.delete(USER_ID);
user = userService.get(USER_ID);
log.debug("user=" + user);
Assert.assertEquals(userService.DEFAULT_NAME, user.getName());
// 即使 cache 中沒(méi)有該數(shù)據(jù),也會(huì)執(zhí)行 delete 中的邏輯
userService.delete(USER_ID);
userService.delete(USER_ID);
}
}
附錄
本文中的完整代碼見(jiàn) spring-boot-redis。