前邊我們已經(jīng)學(xué)習(xí)了 Redis 的一些基本命令贱鼻,以及通過 Jedis桃笙、Lettuce 來操作 Redis冻璃,但在實(shí)際的開發(fā)中评肆,我們更多的會(huì)在 SpringBoot 中整合 Redis,來提高開發(fā)效率陕赃。
一卵蛉、集成 Redis
我這里使用 SpringBoot 2.5.0版本,通過 Spring Data Redis 來集成 Redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后就是一些 Redis 的配置:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=shehuan
從 SpringBoot2.x 開始么库,默認(rèn)使用 Lettuce 作為 Spring Data Redis 的內(nèi)部實(shí)現(xiàn)傻丝,而不是 Jedis,這一點(diǎn)可以從spring-boot-starter-data-redis
的 pom 文件看出:
如果需要使用 Jedis诉儒,則需要手動(dòng)添加對(duì)應(yīng)的依賴:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.0</version>
</dependency>
并在配置文件中切換 Redis 客戶端為 jedis:
spring.redis.client-type=jedis
最基本的配置就這些了葡缰,根據(jù) SpringBoot 的自動(dòng)裝配機(jī)制,會(huì)自動(dòng)的創(chuàng)建一些對(duì)象來方便我們操作 Redis:
-
RedisConnectionFactory
忱反,就是根據(jù)指定的配置來獲取 Redis 連接的 -
RedisTemplate
泛释、StringRedisTemplate
,用來操作 Redis 存取數(shù)據(jù)的温算,既然這兩個(gè)都是用來存取數(shù)據(jù)的怜校,那肯定是有區(qū)別的,下邊我們具體看一下米者。
二韭畸、RedisTemplate
在 Redis 中宇智,StringRedisTemplate
是專門用來存蔓搞、取字符串類型數(shù)據(jù)的,它繼承RedisTemplate
随橘,使用StringRedisSerializer
作為序列化器喂分。
RedisTemplate
可以用來存、取自定義的復(fù)雜數(shù)據(jù)類型机蔗,當(dāng)然也包括字符串類型蒲祈,它默認(rèn)使用JdkSerializationRedisSerializer
作為 Redis 中 key甘萧、value 的序列化器,但是這個(gè)序列化器會(huì)先將 key梆掸、value 序列化成字節(jié)數(shù)組然后再存儲(chǔ)到 Redis扬卷,導(dǎo)致無法通過 Redis 客戶端直觀的看出到底存儲(chǔ)的是什么信息,有問題也就不好排查了酸钦,例如我們存儲(chǔ)一個(gè)User
對(duì)象:
public class User implements Serializable {
private Interger id;
private String name;
private Integer age;
public User() {
}
public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
// 省略get怪得、set
}
@Service
public class MyRedisService {
@Autowired
RedisTemplate<Object, Object> redisTemplate;
public void test1() {
redisTemplate.opsForValue().set("user", new User("zhangsan", 18));
}
}
通過測(cè)試類執(zhí)行程序:
@SpringBootTest
public class MyRedisApplicationTests {
@Autowired
MyRedisService myRedisService;
@Test
void contextLoads() {
myRedisService.test1();
}
}
然后在客戶端查看數(shù)據(jù),紅色區(qū)域分別是存進(jìn)去的 key卑硫、value:
為了解決這個(gè)問題徒恋,一般需要我們自定義RedisTemplate
來覆蓋框架生成的。設(shè)置 key 的序列化器為StringRedisSerializer
欢伏,即將 key 序列化為字符串入挣;至于 value 的序列化器可以使用默認(rèn)的JdkSerializationRedisSerializer
,也可以設(shè)置為Jackson2JsonRedisSerializer
硝拧,即將 value 序列化為 json 字符串在存儲(chǔ)径筏。由于 Redis 中 Hash 類型數(shù)據(jù)結(jié)構(gòu)的 value 也是一個(gè) field-value 鍵值對(duì),也可以分別指定序列化器障陶。
@Configuration
public class RedisConfig {
@Bean("redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 設(shè)置連接工廠
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 定義 String 序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 定義 Jackson 序列化器
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//反序列化時(shí)智能識(shí)別變量名(識(shí)別沒有按駝峰格式命名的變量名)
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//反序列化識(shí)別對(duì)象類型
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
//反序列化如果有多的屬性匠璧,不拋出異常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化如果碰到不識(shí)別的枚舉值,是否作為空值解釋咸这,true:不會(huì)拋不識(shí)別的異常, 會(huì)賦空值夷恍,false:會(huì)拋不識(shí)別的異常
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 設(shè)置 Redis 的 key 以及 hash 結(jié)構(gòu)的 field 使用 String 序列化器
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// 設(shè)置 Redis 的 value 以及 hash 結(jié)構(gòu)的 value 使用 Jackson 序列化器
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
清空數(shù)據(jù),再次運(yùn)行測(cè)試代碼后媳维,再查看客戶端數(shù)據(jù)酿雪,基本符合預(yù)期了:
注意,RedisTemplate
使用JdkSerializationRedisSerializer
作為 value 的默認(rèn)序列化器侄刽,直接存儲(chǔ)數(shù)字或者以字符串形式存儲(chǔ)數(shù)字指黎,后期都是無法使用 Redis 命令對(duì) value 進(jìn)行各種數(shù)學(xué)運(yùn)算的;使用Jackson2JsonRedisSerializer
作為 value 的序列化器時(shí)直接存儲(chǔ)數(shù)字州丹,是可以對(duì)value 進(jìn)行數(shù)學(xué)運(yùn)算的醋安;StringRedisTemplate
使用StringRedisSerializer
作為默認(rèn)的序列化器,以字符串形式存儲(chǔ)的數(shù)字后期是可以進(jìn)行數(shù)學(xué)運(yùn)算的墓毒。
三吓揪、操作 Redis 數(shù)據(jù)類型
如果要存取的數(shù)據(jù)可以用字符串類型表示,建議使用StringRedisTemplate
所计,如果是自定義的復(fù)雜對(duì)象可以使用RedisTemplate
柠辞,這里的RedisTemplate
是前邊我們自定義的。
Spring Data Redis 中提供了如下接口主胧,可以完成對(duì) Redis 常見數(shù)據(jù)結(jié)構(gòu)的操作:
-
ValueOperations
,對(duì)應(yīng) String 數(shù)據(jù)類型,bit(bitmap/位圖)操作也是用它實(shí)現(xiàn) -
ListOperations
哑姚,對(duì)應(yīng) List 數(shù)據(jù)類型 -
SetOperations
,對(duì)應(yīng) Set 數(shù)據(jù)類型 -
HashOperations
图毕,對(duì)應(yīng) Hash 數(shù)據(jù)類型 -
ZSetOperations
,對(duì)應(yīng) ZSet 數(shù)據(jù)類型 -
GeoOperations
眷唉,對(duì)應(yīng) Geo 數(shù)據(jù)類型 -
HyperLogLogOperations
吴旋,對(duì)應(yīng) HyperLogLog 數(shù)據(jù)類型
具體的用法也是很簡單的,和 Redis 數(shù)據(jù)類型的用法基本一致厢破,下邊舉幾個(gè)例子:
@Service
public class MyRedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate<Object, Object> redisTemplate;
public void test2() {
stringRedisTemplate.opsForValue().set("key1", "10");
String key1 = stringRedisTemplate.opsForValue().get("key1");
stringRedisTemplate.opsForSet().add("key2", "1", "2", "3");
Boolean isMember = stringRedisTemplate.opsForSet().isMember("key2", "1");
redisTemplate.opsForList().leftPush("user", new User("zhangsan", 18));
redisTemplate.opsForList().leftPush("user", new User("lisi", 20));
User user1 = (User) redisTemplate.opsForList().rightPop("user");
}
}
使用XxxxOperations
系列的接口荣瑟,如果要對(duì)一個(gè) key 的值進(jìn)行多次操作,就需要多次綁定同一個(gè) key摩泪,會(huì)麻煩一些笆焰。
針對(duì)這種情況,我們可以使用BoundKeyOperations
接口的實(shí)現(xiàn)類來實(shí)現(xiàn)對(duì)一個(gè) key 的值進(jìn)行多次操作:
BoundValueOperations
BoundListOperations
BoundSetOperations
BoundHashOperations
BoundZSetOperations
BoundGeoOperations
public void test3() {
BoundValueOperations<String, String> boundValueOperations = stringRedisTemplate.boundValueOps("key1");
boundValueOperations.set("10");
String key1 = boundValueOperations.get();
BoundSetOperations<String, String> boundSetOperations = stringRedisTemplate.boundSetOps("key2");
boundSetOperations.add("1", "2", "3");
Boolean isMember = boundSetOperations.isMember("1");
BoundListOperations<Object, Object> boundListOperations = redisTemplate.boundListOps("user");
boundListOperations.leftPush(new User("zhangsan", 18));
boundListOperations.leftPush(new User("lisi", 20));
User user1 = (User) boundListOperations.rightPop();
}
四见坑、事務(wù)
之前的文章我們已經(jīng)知道嚷掠,事務(wù)中常用的命令有watch
、unwatch
荞驴、multi
不皆、exec
,在 SpringBoot 也是類似的熊楼,但由于事務(wù)中往往涉及多個(gè)命令霹娄,我們要保證在同一個(gè)連接中執(zhí)行所有的命令,這時(shí)需要用到SessionCallback
接口鲫骗,之前文章中我們用 Jedis 實(shí)現(xiàn)了事務(wù)犬耻,這里我們?cè)谠踊A(chǔ)上修改為 SpringBoot 整合 Redis 后事務(wù)的用法:
@Service
public class MyRedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
public void test4() {
// 設(shè)置商品庫存為1000件
stringRedisTemplate.opsForValue().set("stock", "1");
List<Object> results = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
// 監(jiān)控庫存
operations.watch("stock");
// 獲取庫存
int stock = Integer.parseInt(String.valueOf(operations.opsForValue().get("stock")));
// 如果庫存大于購買數(shù)量
if (stock > 10) {
stock = stock - 10;
} else {
// 取消監(jiān)控
operations.unwatch();
return null;
}
// 開啟事務(wù)
operations.multi();
//減扣庫存
operations.opsForValue().set("stock", String.valueOf(stock));
// 執(zhí)行事務(wù),此處打斷點(diǎn)执泰,在客戶端修改庫存
List<Object> results = operations.exec();
// 如果事務(wù)執(zhí)行過程中發(fā)現(xiàn)庫存在其它地方被修改過枕磁,則返回List的大小為0
return results;
}
});
if (results == null || results.size() == 0) {
System.out.println("庫存減扣失敗术吝!");
} else {
System.out.println("剩余庫存:" + stringRedisTemplate.opsForValue().get("stock"));
}
}
}
五计济、pipeline
前邊這些例子中,Redis 命令都是逐條發(fā)送到服務(wù)器去執(zhí)行的排苍,這是 Redis 的默認(rèn)策略沦寂,如果有大量的命令需要執(zhí)行,這樣效率顯然是不高的纪岁,許多時(shí)間都會(huì)耗費(fèi)在網(wǎng)絡(luò)傳輸上凑队≡蚬基于這樣的情況幔翰,我們可以使用pipeline
技術(shù)來優(yōu)化漩氨,將指令批量發(fā)送到服務(wù)器去執(zhí)行,提高效率遗增。具體的用法如下:
@Service
public class MyRedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
public void test5() {
stringRedisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
// 寫在這里的命令會(huì)被批量發(fā)送到服務(wù)器執(zhí)行
return null;
}
});
}
}
六叫惊、緩存
一般數(shù)據(jù)庫操作都是效率比較低的,容易產(chǎn)生性能問題做修,可以將一些從數(shù)據(jù)庫查出的數(shù)據(jù)緩存起來霍狰,重復(fù)利用,提高性能饰及。在 Sping3.1 中引入了緩存(Cache)的功能蔗坯,Sping 的緩存功能支持多種實(shí)現(xiàn),Redis 是比較常用的燎含,還有Ehcache
等其它的宾濒,這里就不介紹了,用法基本一致屏箍。SpringBoot 整合 Redis 后绘梦,可以很方便的用 Redis 作為緩存的實(shí)現(xiàn)方式,實(shí)現(xiàn)數(shù)據(jù)的緩存赴魁。
除了上邊 Redis 連接相關(guān)的配置外卸奉,還需要額外添加使用 Redis 作為緩存需要的配置:
# 指定緩存類型
spring.cache.type=redis
# 緩存超時(shí)時(shí)間,0為永不超時(shí)
spring.cache.redis.time-to-live=0ms
以及 Spring 緩存的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
在 SpringBoot 啟動(dòng)類上添加開啟緩存的注解@EnableCaching
:
@SpringBootApplication
@EnableCaching
public class MyRedisApplication {
public static void main(String[] args) {
SpringApplication.run(MyRedisApplication.class, args);
}
}
接下來就是如何去緩存數(shù)據(jù)了颖御,這里涉及到如下幾個(gè)注解:
-
@CacheConfig
榄棵,在類上使用,表示該類中方法使用的緩存名稱(可以理解為數(shù)據(jù)緩存的命名空間)潘拱,除了在類上使用該注解配置緩存名稱秉继,還可以用下邊三個(gè)注解在方法上配置 -
@CachePut
,一般用在新增或更新業(yè)務(wù)的方法上泽铛,當(dāng)數(shù)據(jù)新增或更新成功后尚辑,將方法的返回結(jié)果使用指定的 key 添加到緩存中,或更新緩存中已有的 key 的值 -
@Cacheable
盔腔,一般用在查詢業(yè)務(wù)的方法上杠茬,先從緩存中根據(jù)指定的 key 查詢數(shù)據(jù),如果查詢到就直接返回弛随,否則執(zhí)行該方法來獲取數(shù)據(jù)瓢喉,最后將方法的返回結(jié)果保存到緩存 -
@CacheEvict
,一般用在刪除業(yè)務(wù)的方法上舀透,默認(rèn)會(huì)在方法執(zhí)行結(jié)束后移除指定 key 對(duì)應(yīng)的緩存數(shù)據(jù)
下邊用一個(gè)例子具體看如何使用這些注解:
@Service
@CacheConfig(cacheNames = "cache1")
public class UserService {
@Autowired
UserDao userDao;
@Cacheable(cacheNames = "cache2", key = "'user'+#id")
public User getUserById(String id) {
return userDao.getUserById(id);
}
@CachePut(key = "'user'+#user.id")
public User addUser(User user) {
return userDao.addUser(user);
}
@CachePut(key = "'user'+#user.id", condition = "#result != 'null'")
public User updateUser(User user) {
if (userDao.getUserById(user.getId()) == null) {
return null;
}
return userDao.updateUser(user);
}
@CacheEvict(key = "'user'+#id")
public Integer deleteUserById(String id) {
return userDao.deleteUserById(id);
}
}
針對(duì)這個(gè)例子做一些說明:
-
UserDao
是用來模擬數(shù)據(jù)庫操作的栓票,里邊的內(nèi)容不重要。 - 注解的
cacheNames
屬性用來配置緩存的名稱,方法上的配置會(huì)覆蓋類上的配置走贪。 -
@Cacheable
佛猛、@CachePut
、@CacheEvict
都配置了一個(gè) key 屬性坠狡,作為 Redis 中緩存數(shù)據(jù)的 key继找,key 的值是通過一個(gè) Spring EL 表達(dá)式返回的,這樣可以根據(jù)實(shí)際需求自由的指定 key 的值逃沿。 -
@CachePut
還配置了一個(gè)condition
屬性婴渡,用作條判斷,這里表示方法返回的結(jié)果不為 null 才緩存數(shù)據(jù)凯亮。當(dāng)然你也可以在@Cacheable
边臼、@CacheEvict
中condition
屬性,以便在滿足對(duì)應(yīng)條件時(shí)才對(duì)緩存做相應(yīng)操作假消。
最后做一個(gè)簡單的測(cè)試:
@SpringBootTest
class MyRedisApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
// 查詢用戶
userService.getUserById(100);
// 添加用戶
User user = new User(102, "wangwu", 19);
userService.addUser(user);
}
}
在 Redis 客戶端查看緩存的數(shù)據(jù):