摘要
Spring Data Redis 是Spring 框架提供的用于操作Redis的方式直砂,最近整理了下它的用法,解決了使用過程中遇到的一些難點與坑點汹买,希望對大家有所幫助佩伤。本文涵蓋了Redis的安裝、Spring Cache結(jié)合Redis的使用晦毙、Redis連接池的使用和RedisTemplate的使用等內(nèi)容生巡。
Redis安裝
這里提供Linux和Windows兩種安裝方式,由于Windows下的版本最高只有3.2版本见妒,所以推薦使用Linux下的版本孤荣,目前最新穩(wěn)定版本為5.0,也是本文中使用的版本。
Linux
這里我們使用Docker環(huán)境下的安裝方式盐股。
下載Redis5.0的Docker鏡像钱豁;
docker pull redis:5.0
使用Docker命令啟動Redis容器;
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-d redis:5.0 redis-server --appendonly yes
Windows
想使用Windows版本的朋友可以使用以下安裝方式疯汁。
下載Windows版本的Redis,下載地址:github.com/MicrosoftAr…
下載完后解壓到指定目錄牲尺;
在當前地址欄輸入cmd后,執(zhí)行redis的啟動命令:redis-server.exe redis.windows.conf
Spring Cache 操作Redis
Spring Cache 簡介
當Spring Boot 結(jié)合Redis來作為緩存使用時幌蚊,最簡單的方式就是使用Spring Cache了谤碳,使用它我們無需知道Spring中對Redis的各種操作,僅僅通過它提供的@Cacheable 溢豆、@CachePut 蜒简、@CacheEvict 、@EnableCaching等注解就可以實現(xiàn)緩存功能漩仙。
常用注解
@EnableCaching
開啟緩存功能臭蚁,一般放在啟動類上。
@Cacheable
使用該注解的方法當緩存存在時讯赏,會從緩存中獲取數(shù)據(jù)而不執(zhí)行方法垮兑,當緩存不存在時,會執(zhí)行方法并把返回結(jié)果存入緩存中漱挎。一般使用在查詢方法上系枪,可以設置如下屬性:
value:緩存名稱(必填),指定緩存的命名空間磕谅;
key:用于設置在命名空間中的緩存key值私爷,可以使用SpEL表達式定義;
unless:條件符合則不緩存膊夹;
condition:條件符合則緩存衬浑。
@CachePut
使用該注解的方法每次執(zhí)行時都會把返回結(jié)果存入緩存中。一般使用在新增方法上放刨,可以設置如下屬性:
value:緩存名稱(必填)工秩,指定緩存的命名空間;
key:用于設置在命名空間中的緩存key值进统,可以使用SpEL表達式定義助币;
unless:條件符合則不緩存;
condition:條件符合則緩存螟碎。
@CacheEvict
使用該注解的方法執(zhí)行時會清空指定的緩存眉菱。一般使用在更新或刪除方法上,可以設置如下屬性:
value:緩存名稱(必填)掉分,指定緩存的命名空間俭缓;
key:用于設置在命名空間中的緩存key值克伊,可以使用SpEL表達式定義;
condition:條件符合則緩存华坦。
使用步驟
在pom.xml中添加項目依賴:
org.springframework.boot
spring-boot-starter-data-redis
修改配置文件application.yml愿吹,添加Redis的連接配置;
spring:
redis:
host: 192.168.6.139 # Redis服務器地址
database: 0 # Redis數(shù)據(jù)庫索引(默認為0)
port: 6379 # Redis服務器連接端口
password: # Redis服務器連接密碼(默認為空)
timeout: 1000ms # 連接超時時間
在啟動類上添加@EnableCaching注解啟動緩存功能季春;
@EnableCaching
@SpringBootApplication
public class MallTinyApplication {
public static void main(String[] args) {
SpringApplication.run(MallTinyApplication.class, args);
}
}
接下來在PmsBrandServiceImpl類中使用相關注解來實現(xiàn)緩存功能,可以發(fā)現(xiàn)我們獲取品牌詳情的方法中使用了@Cacheable注解消返,在修改和刪除品牌的方法上使用了@CacheEvict注解载弄;
/**
* PmsBrandService實現(xiàn)類
* Created by macro on 2019/4/19.
*/
@Service
public class PmsBrandServiceImpl implements PmsBrandService {
@Autowired
private PmsBrandMapper brandMapper;
@CacheEvict(value = RedisConfig.REDIS_KEY_DATABASE, key = "'pms:brand:'+#id")
@Override
public int update(Long id, PmsBrand brand) {
brand.setId(id);
return brandMapper.updateByPrimaryKeySelective(brand);
}
@CacheEvict(value = RedisConfig.REDIS_KEY_DATABASE, key = "'pms:brand:'+#id")
@Override
public int delete(Long id) {
return brandMapper.deleteByPrimaryKey(id);
}
@Cacheable(value = RedisConfig.REDIS_KEY_DATABASE, key = "'pms:brand:'+#id", unless = "#result==null")
@Override
public PmsBrand getItem(Long id) {
return brandMapper.selectByPrimaryKey(id);
}
}
我們可以調(diào)用獲取品牌詳情的接口測試下效果,此時發(fā)現(xiàn)Redis中存儲的數(shù)據(jù)有點像亂碼撵颊,并且沒有設置過期時間宇攻;
存儲JSON格式數(shù)據(jù)
此時我們就會想到有沒有什么辦法讓Redis中存儲的數(shù)據(jù)變成標準的JSON格式,然后可以設置一定的過期時間倡勇,不設置過期時間容易產(chǎn)生很多不必要的緩存數(shù)據(jù)逞刷。
我們可以通過給RedisTemplate設置JSON格式的序列化器,并通過配置RedisCacheConfiguration設置超時時間來實現(xiàn)以上需求妻熊,此時別忘了去除啟動類上的@EnableCaching注解夸浅,具體配置類RedisConfig代碼如下;
/**
* Redis配置類
* Created by macro on 2020/3/2.
*/
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
/**
* redis數(shù)據(jù)庫自定義key
*/
public? static final String REDIS_KEY_DATABASE="mall";
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer serializer = redisSerializer();
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisSerializer redisSerializer() {
//創(chuàng)建JSON序列化器
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
return serializer;
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
//設置Redis緩存有效期為1天
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
}
此時我們再次調(diào)用獲取商品詳情的接口進行測試扔役,會發(fā)現(xiàn)Redis中已經(jīng)緩存了標準的JSON格式數(shù)據(jù)帆喇,并且超時時間被設置為了1天。
使用Redis連接池
SpringBoot 1.5.x版本Redis客戶端默認是Jedis實現(xiàn)的亿胸,SpringBoot 2.x版本中默認客戶端是用Lettuce實現(xiàn)的坯钦,我們先來了解下Jedis和Lettuce客戶端。
Jedis vs Lettuce
Jedis在實現(xiàn)上是直連Redis服務侈玄,多線程環(huán)境下非線程安全婉刀,除非使用連接池,為每個 RedisConnection 實例增加物理連接序仙。
Lettuce是一種可伸縮突颊,線程安全,完全非阻塞的Redis客戶端潘悼,多個線程可以共享一個RedisConnection洋丐,它利用Netty NIO框架來高效地管理多個連接,從而提供了異步和同步數(shù)據(jù)訪問方式挥等,用于構(gòu)建非阻塞的反應性應用程序友绝。
使用步驟
修改application.yml添加Lettuce連接池配置,用于配置線程數(shù)量和阻塞等待時間肝劲;
spring:
redis:
lettuce:
pool:
max-active: 8 # 連接池最大連接數(shù)
max-idle: 8 # 連接池最大空閑連接數(shù)
min-idle: 0 # 連接池最小空閑連接數(shù)
max-wait: -1ms # 連接池最大阻塞等待時間迁客,負值表示沒有限制
由于SpringBoot 2.x中默認并沒有使用Redis連接池郭宝,所以需要在pom.xml中添加commons-pool2的依賴;
org.apache.commons
commons-pool2
如果你沒添加以上依賴的話掷漱,啟動應用的時候就會產(chǎn)生如下錯誤粘室;
Caused by: java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig
at org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration$LettucePoolingClientConfigurationBuilder.(LettucePoolingClientConfiguration.java:84) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration.builder(LettucePoolingClientConfiguration.java:48) ~[spring-data-redis-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration$PoolBuilderFactory.createBuilder(LettuceConnectionConfiguration.java:149) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration.createBuilder(LettuceConnectionConfiguration.java:107) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration.getLettuceClientConfiguration(LettuceConnectionConfiguration.java:93) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration.redisConnectionFactory(LettuceConnectionConfiguration.java:74) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration$$EnhancerBySpringCGLIB$$5caa7e47.CGLIB$redisConnectionFactory$0() ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration$$EnhancerBySpringCGLIB$$5caa7e47$$FastClassBySpringCGLIB$$b8ae2813.invoke() ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:363) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration$$EnhancerBySpringCGLIB$$5caa7e47.redisConnectionFactory() ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_91]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_91]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_91]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_91]
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
... 111 common frames omitted
自由操作Redis
Spring Cache 給我們提供了操作Redis緩存的便捷方法,但是也有很多局限性卜范。比如說我們想單獨設置一個緩存值的有效期怎么辦衔统?我們并不想緩存方法的返回值,我們想緩存方法中產(chǎn)生的中間值怎么辦海雪?此時我們就需要用到RedisTemplate這個類了锦爵,接下來我們來講下如何通過RedisTemplate來自由操作Redis中的緩存。
RedisService
定義Redis操作業(yè)務類奥裸,在Redis中有幾種數(shù)據(jù)結(jié)構(gòu)险掀,比如普通結(jié)構(gòu)(對象),Hash結(jié)構(gòu)湾宙、Set結(jié)構(gòu)樟氢、List結(jié)構(gòu),該接口中定義了大多數(shù)常用操作方法侠鳄。
/**
* redis操作Service
* Created by macro on 2020/3/3.
*/
public interface RedisService {
/**
* 保存屬性
*/
void set(String key, Object value, long time);
/**
* 保存屬性
*/
void set(String key, Object value);
/**
* 獲取屬性
*/
Object get(String key);
/**
* 刪除屬性
*/
Boolean del(String key);
/**
* 批量刪除屬性
*/
Long del(List keys);
/**
* 設置過期時間
*/
Boolean expire(String key, long time);
/**
* 獲取過期時間
*/
Long getExpire(String key);
/**
* 判斷是否有該屬性
*/
Boolean hasKey(String key);
/**
* 按delta遞增
*/
Long incr(String key, long delta);
/**
* 按delta遞減
*/
Long decr(String key, long delta);
/**
* 獲取Hash結(jié)構(gòu)中的屬性
*/
Object hGet(String key, String hashKey);
/**
* 向Hash結(jié)構(gòu)中放入一個屬性
*/
Boolean hSet(String key, String hashKey, Object value, long time);
/**
* 向Hash結(jié)構(gòu)中放入一個屬性
*/
void hSet(String key, String hashKey, Object value);
/**
* 直接獲取整個Hash結(jié)構(gòu)
*/
Map hGetAll(String key);
/**
* 直接設置整個Hash結(jié)構(gòu)
*/
Boolean hSetAll(String key, Map map, long time);
/**
* 直接設置整個Hash結(jié)構(gòu)
*/
void hSetAll(String key, Map map);
/**
* 刪除Hash結(jié)構(gòu)中的屬性
*/
void hDel(String key, Object... hashKey);
/**
* 判斷Hash結(jié)構(gòu)中是否有該屬性
*/
Boolean hHasKey(String key, String hashKey);
/**
* Hash結(jié)構(gòu)中屬性遞增
*/
Long hIncr(String key, String hashKey, Long delta);
/**
* Hash結(jié)構(gòu)中屬性遞減
*/
Long hDecr(String key, String hashKey, Long delta);
/**
* 獲取Set結(jié)構(gòu)
*/
Set sMembers(String key);
/**
* 向Set結(jié)構(gòu)中添加屬性
*/
Long sAdd(String key, Object... values);
/**
* 向Set結(jié)構(gòu)中添加屬性
*/
Long sAdd(String key, long time, Object... values);
/**
* 是否為Set中的屬性
*/
Boolean sIsMember(String key, Object value);
/**
* 獲取Set結(jié)構(gòu)的長度
*/
Long sSize(String key);
/**
* 刪除Set結(jié)構(gòu)中的屬性
*/
Long sRemove(String key, Object... values);
/**
* 獲取List結(jié)構(gòu)中的屬性
*/
List lRange(String key, long start, long end);
/**
* 獲取List結(jié)構(gòu)的長度
*/
Long lSize(String key);
/**
* 根據(jù)索引獲取List中的屬性
*/
Object lIndex(String key, long index);
/**
* 向List結(jié)構(gòu)中添加屬性
*/
Long lPush(String key, Object value);
/**
* 向List結(jié)構(gòu)中添加屬性
*/
Long lPush(String key, Object value, long time);
/**
* 向List結(jié)構(gòu)中批量添加屬性
*/
Long lPushAll(String key, Object... values);
/**
* 向List結(jié)構(gòu)中批量添加屬性
*/
Long lPushAll(String key, Long time, Object... values);
/**
* 從List結(jié)構(gòu)中移除屬性
*/
Long lRemove(String key, long count, Object value);
}
RedisServiceImpl
RedisService的實現(xiàn)類,使用RedisTemplate來自由操作Redis中的緩存數(shù)據(jù)伟恶。
/**
* redis操作實現(xiàn)類
* Created by macro on 2020/3/3.
*/
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void set(String key, Object value, long time) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
@Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public Boolean del(String key) {
return redisTemplate.delete(key);
}
@Override
public Long del(List keys) {
return redisTemplate.delete(keys);
}
@Override
public Boolean expire(String key, long time) {
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
@Override
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
@Override
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
@Override
public Long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
@Override
public Long decr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, -delta);
}
@Override
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
@Override
public Boolean hSet(String key, String hashKey, Object value, long time) {
redisTemplate.opsForHash().put(key, hashKey, value);
return expire(key, time);
}
@Override
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
@Override
public Map hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
@Override
public Boolean hSetAll(String key, Map map, long time) {
redisTemplate.opsForHash().putAll(key, map);
return expire(key, time);
}
@Override
public void hSetAll(String key, Map map) {
redisTemplate.opsForHash().putAll(key, map);
}
@Override
public void hDel(String key, Object... hashKey) {
redisTemplate.opsForHash().delete(key, hashKey);
}
@Override
public Boolean hHasKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
@Override
public Long hIncr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, delta);
}
@Override
public Long hDecr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, -delta);
}
@Override
public Set sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
@Override
public Long sAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
@Override
public Long sAdd(String key, long time, Object... values) {
Long count = redisTemplate.opsForSet().add(key, values);
expire(key, time);
return count;
}
@Override
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
@Override
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
}
@Override
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
@Override
public List lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
@Override
public Long lSize(String key) {
return redisTemplate.opsForList().size(key);
}
@Override
public Object lIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
@Override
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
}
@Override
public Long lPush(String key, Object value, long time) {
Long index = redisTemplate.opsForList().rightPush(key, value);
expire(key, time);
return index;
}
@Override
public Long lPushAll(String key, Object... values) {
return redisTemplate.opsForList().rightPushAll(key, values);
}
@Override
public Long lPushAll(String key, Long time, Object... values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
expire(key, time);
return count;
}
@Override
public Long lRemove(String key, long count, Object value) {
return redisTemplate.opsForList().remove(key, count, value);
}
}
RedisController
測試RedisService中緩存操作的Controller霸妹,大家可以調(diào)用測試下。
/**
* Redis測試Controller
* Created by macro on 2020/3/3.
*/
@Api(tags = "RedisController", description = "Redis測試")
@Controller
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisService redisService;
@Autowired
private PmsBrandService brandService;
@ApiOperation("測試簡單緩存")
@RequestMapping(value = "/simpleTest", method = RequestMethod.GET)
@ResponseBody
public CommonResult simpleTest() {
List brandList = brandService.list(1, 5);
PmsBrand brand = brandList.get(0);
String key = "redis:simple:" + brand.getId();
redisService.set(key, brand);
PmsBrand cacheBrand = (PmsBrand) redisService.get(key);
return CommonResult.success(cacheBrand);
}
@ApiOperation("測試Hash結(jié)構(gòu)的緩存")
@RequestMapping(value = "/hashTest", method = RequestMethod.GET)
@ResponseBody
public CommonResult hashTest() {
List brandList = brandService.list(1, 5);
PmsBrand brand = brandList.get(0);
String key = "redis:hash:" + brand.getId();
Map value = BeanUtil.beanToMap(brand);
redisService.hSetAll(key, value);
Map cacheValue = redisService.hGetAll(key);
PmsBrand cacheBrand = BeanUtil.mapToBean(cacheValue, PmsBrand.class, true);
return CommonResult.success(cacheBrand);
}
@ApiOperation("測試Set結(jié)構(gòu)的緩存")
@RequestMapping(value = "/setTest", method = RequestMethod.GET)
@ResponseBody
public CommonResult> setTest() {
List brandList = brandService.list(1, 5);
String key = "redis:set:all";
redisService.sAdd(key, (Object[]) ArrayUtil.toArray(brandList, PmsBrand.class));
redisService.sRemove(key, brandList.get(0));
Set cachedBrandList = redisService.sMembers(key);
return CommonResult.success(cachedBrandList);
}
@ApiOperation("測試List結(jié)構(gòu)的緩存")
@RequestMapping(value = "/listTest", method = RequestMethod.GET)
@ResponseBody
public CommonResult> listTest() {
List brandList = brandService.list(1, 5);
String key = "redis:list:all";
redisService.lPushAll(key, (Object[]) ArrayUtil.toArray(brandList, PmsBrand.class));
redisService.lRemove(key, 1, brandList.get(0));
List cachedBrandList = redisService.lRange(key, 0, 3);
return CommonResult.success(cachedBrandList);
}
}