作为开发者,相信大家都知道 Redis 的重要性。Redis 是使用 C 语言开发的一个高性能键值对数据库,是互联网技术领域使用最为广泛的存储中间件,它是「Remote Dictionary Service」的首字母缩写,也就是「远程字典服务」。
Redis 以超高的性能、完美的文档、简洁的源码著称,国内外很多大型互联网公司都在用,比如说阿里、腾讯、GitHub、Stack Overflow 等等。当然了,中小型公司也都在用。
安装 Redis
Redis 的官网提供了各种平台的安装包,Linux、macOS、Windows 的都有。
官方地址:https://redis.io/docs/install/
完成安装后执行 redis-server
就可以启动 Redis 服务了。
顺带安装一下 Redis 客户端工具,推荐 GitHub 星标 20k+ 的 AnotherRedisDesktopManager,一款 🚀🚀🚀 更快、更好、更稳定的Redis桌面(GUI)管理客户端,支持 Windows、macOS 和 Linux,性能出众,可以轻松加载海量键值。
https://github.com/qishibo/AnotherRedisDesktopManager
安装完成后,链接 Redis 服务
Redis 数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
Redis 教程:Redis 字符串(String)_redis教程
Spring Boot 整合 Redis
第一步,在 pom.xml 文件中添加 Redis 的 starter。
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步,在 application.yml 文件中添加 Redis 的配置信息
html
spring:
redis:
host: xxx.xxx.99.232 # Redis服务器地址
database: 0 # Redis数据库索引(默认为0)
port: 6379 # Redis服务器连接端口
password: xxx # Redis服务器连接密码(默认为空)
第三步,在测试类中添加以下代码。
java
@SpringBootTest
class CodingmoreRedisApplicationTests {
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
public void testRedis() {
// 添加
redisTemplate.opsForValue().set("name","欧尼甲");
// 查询
System.out.println(redisTemplate.opsForValue().get("name"));
// 删除
redisTemplate.delete("name");
// 更新
redisTemplate.opsForValue().set("name","哈哈好傻");
// 查询
System.out.println(redisTemplate.opsForValue().get("name"));
// 添加
stringRedisTemplate.opsForValue().set("name","欧尼甲");
// 查询
System.out.println(stringRedisTemplate.opsForValue().get("name"));
// 删除
stringRedisTemplate.delete("name");
// 更新
stringRedisTemplate.opsForValue().set("name","哈哈好傻");
// 查询
System.out.println(stringRedisTemplate.opsForValue().get("name"));
}
}
RedisTemplate 和 StringRedisTemplate 都是 Spring Data Redis 提供的模板类,可以对 Redis 进行操作,后者针对键值对都是 String 类型的数据,前者可以是任何类型的对象。
RedisTemplate 和 StringRedisTemplate 除了提供 opsForValue 方法来操作字符串之外,还提供了以下方法:
- opsForList:操作 list
- opsForSet:操作 set
- opsForZSet:操作有序 set
- opsForHash:操作 hash
运行测试类后可以在控制台看到相关信息。
也可以通过 AnotherRedisDesktopManager 客户端查看 Redis 数据库中的数据
编程喵整合 Redis
编程喵是一个 Spring Boot + Vue 的前后端分离项目,要整合 Redis 的话,最好的方式是使用 Spring Cache,仅仅通过 @Cacheable、@CachePut、@CacheEvict、@EnableCaching 等注解就可以轻松使用 Redis 做缓存了
1) @EnableCaching,开启缓存功能。
2) @Cacheable,调用方法前,去缓存中找,找到就返回,找不到就执行方法,并将返回值放到缓存中。
3) @CachePut,方法调用前不会去缓存中找,无论如何都会执行方法,执行完将返回值放到缓存中。
4) @CacheEvict,清理缓存中的一个或多个记录。
Spring Cache 是 Spring 提供的一套完整的缓存解决方案,虽然它本身没有提供缓存的实现,但它提供的一整套接口、规范、配置、注解等,可以让我们无缝衔接 Redis、Ehcache 等缓存实现。
Spring Cache 的注解(前面提到的四个)会在调用方法之后,去缓存方法返回的最终结果;或者在方法调用之前拿缓存中的结果,当然还有删除缓存中的结果。
这些读写操作不用我们手动再去写代码实现了,直接交给 Spring Cache 来打理就 OK 了,是不是非常贴心?
第一步,在 pom.xml 文件中追加 Redis 的 starter。
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步,在 application.yml 文件中添加 Redis 链接配置。
java
spring:
redis:
host: 118.xx.xx.xxx # Redis服务器地址
database: 0 # Redis数据库索引(默认为0)
port: 6379 # Redis服务器连接端口
password: xx # Redis服务器连接密码(默认为空)
timeout: 1000ms # 连接超时时间(毫秒)
第三步,新增 RedisConfig.java 类,通过 RedisTemplate 设置 JSON 格式的序列化器,这样的话存储到 Redis 里的数据将是有类型的 JSON 数据,例如:
java
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 通过 Jackson 组件进行序列化
RedisSerializer<Object> serializer = redisSerializer();
// key 和 value
// 一般来说, redis-key采用字符串序列化;
// redis-value采用json序列化, json的体积小,可读性高,不需要实现serializer接口。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisSerializer<Object> redisSerializer() {
//创建JSON序列化器
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// https://www.cnblogs.com/shanheyongmu/p/15157378.html
// objectMapper.enableDefaultTyping()被弃用
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
serializer.setObjectMapper(objectMapper);
return serializer;
}
}
通过 RedisCacheConfiguration 设置超时时间,来避免产生很多不必要的缓存数据。
java
@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);
}
第四步 ,在标签更新接口中添加 @CachePut 注解,也就是说方法执行前不会去缓存中找,但方法执行完会将返回值放入缓存中。
java
@Controller
@Api(tags = "标签")
@RequestMapping("/postTag")
public class PostTagController {
@Autowired
private IPostTagService postTagService;
@Autowired
private IPostTagRelationService postTagRelationService;
@RequestMapping(value = "/update", method = RequestMethod.POST)
@ResponseBody
@ApiOperation("修改标签")
@CachePut(value = "codingmore", key = "'codingmore:postags:'+#postAddTagParam.postTagId")
public ResultObject<String> update(@Valid PostTagParam postAddTagParam) {
if (postAddTagParam.getPostTagId() == null) {
return ResultObject.failed("标签id不能为空");
}
PostTag postTag = postTagService.getById(postAddTagParam.getPostTagId());
if (postTag == null) {
return ResultObject.failed("标签不存在");
}
QueryWrapper<PostTag> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("description", postAddTagParam.getDescription());
int count = postTagService.count(queryWrapper);
if (count > 0) {
return ResultObject.failed("标签名称已存在");
}
BeanUtils.copyProperties(postAddTagParam, postTag);
return ResultObject.success(postTagService.updateById(postTag) ? "修改成功" : "修改失败");
}
}
注意看 @CachePut 注解这行代码:
java
@CachePut(value = "codingmore", key = "'codingmore:postags:'+#postAddTagParam.postTagId")
- value:缓存名称,也就是缓存的命名空间,value 这里应该换成 namespace 更好一点;
- key:用于在命名空间中缓存的 key 值,可以使用 SpEL 表达式,比如说
'codingmore:postags:'+#postAddTagParam.postTagId
; - 还有两个属性 unless 和 condition 暂时没用到,分别表示条件符合则不缓存,条件符合则缓存。
第五步,启动服务器端,启动客户端,修改标签进行测试
使用 Redis 连接池
Redis 是基于内存的数据库,本来是为了提高程序性能的,但如果不使用 Redis 连接池的话,建立连接、断开连接就需要消耗大量的时间。
用了连接池,就可以实现在客户端建立多个连接,需要的时候从连接池拿,用完了再放回去,这样就节省了连接建立、断开的时间。
要使用连接池,我们得先了解 Redis 的客户端,常用的有两种:Jedis 和 Lettuce。
- Jedis:Spring Boot 1.5.x 版本时默认的 Redis 客户端,实现上是直接连接 Redis Server,如果在多线程环境下是非线程安全的,这时候要使用连接池为每个 jedis 实例增加物理连接;
- Lettuce:Spring Boot 2.x 版本后默认的 Redis 客户端,基于 Netty 实现,连接实例可以在多个线程间并发访问,一个连接实例不够的情况下也可以按需要增加连接实例。
它俩在 GitHub 上都挺受欢迎的,大家可以按需选用。
我这里把两种客户端的情况都演示一下,方便小伙伴们参考。
1)Lettuce
第一步,修改 application-dev.yml,添加 Lettuce 连接池配置(pool 节点)。
java
spring:
redis:
lettuce:
pool:
max-active: 8 # 连接池最大连接数
max-idle: 8 # 连接池最大空闲连接数
min-idle: 0 # 连接池最小空闲连接数
max-wait: -1ms # 连接池最大阻塞等待时间,负值表示没有限制
第二步,在 pom.xml 文件中添加 commons-pool2 依赖,否则会在启动的时候报 ClassNotFoundException 的错。这是因为 Spring Boot 2.x 里默认没启用连接池。
html
Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 153 common frames omitted
添加 commons-pool2 依赖:
html
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
重新启动服务,在 RedisConfig 类的 redisTemplate 方法里对 redisTemplate 打上断点,debug 模式下可以看到连接池的配置信息(redisConnectionFactory→clientConfiguration→poolConfig
)。如下图所示。
如果在 application-dev.yml 文件中没有添加 Lettuce 连接池配置的话,是不会看到
2)Jedis
第一步,在 pom.xml 文件中添加 Jedis 依赖,去除 Lettuce 默认依赖。
html
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
第二步,修改 application-dev.yml,添加 Jedis 连接池配置。
html
spring:
redis:
jedis:
pool:
max-active: 8 # 连接池最大连接数
max-idle: 8 # 连接池最大空闲连接数
min-idle: 0 # 连接池最小空闲连接数
max-wait: -1ms # 连接池最大阻塞等待时间,负值表示没有限制
启动服务后,观察 redisTemplate 的 clientConfiguration 节点,可以看到它的值已经变成 DefaultJedisClientConfiguration 对象了。
当然了,也可以不配置 Jedis 客户端的连接池,走默认的连接池配置。因为 Jedis 客户端默认增加了连接池的依赖包,在 pom.xml 文件中点开 Jedis 客户端依赖可以查看到。
自由操作 Redis
Spring Cache 虽然提供了操作 Redis 的便捷方法,比如我们前面演示的 @CachePut 注解,但注解提供的操作非常有限,比如说它只能保存返回值到缓存中,而返回值并不一定是我们想要保存的结果。
与其保存这个返回给客户端的 JSON 信息,我们更想保存的是更新后的标签。那该怎么自由地操作 Redis 呢?
第一步,增加 RedisService 接口:
java
package com.codingmore.service;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* redis操作Service
*/
public interface IRedisService {
/**
* 保存属性
*/
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<String> 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结构中的属性
*/
Object hGet(String key, String hashKey);
/**
* 向Hash结构中放入一个属性
*/
Boolean hSet(String key, String hashKey, Object value, long time);
/**
* 向Hash结构中放入一个属性
*/
void hSet(String key, String hashKey, Object value);
/**
* 直接获取整个Hash结构
*/
Map<Object, Object> hGetAll(String key);
/**
* 直接设置整个Hash结构
*/
Boolean hSetAll(String key, Map<String, Object> map, long time);
/**
* 直接设置整个Hash结构
*/
void hSetAll(String key, Map<String, Object> map);
/**
* 删除Hash结构中的属性
*/
void hDel(String key, Object... hashKey);
/**
* 判断Hash结构中是否有该属性
*/
Boolean hHasKey(String key, String hashKey);
/**
* Hash结构中属性递增
*/
Long hIncr(String key, String hashKey, Long delta);
/**
* Hash结构中属性递减
*/
Long hDecr(String key, String hashKey, Long delta);
/**
* 获取Set结构
*/
Set<Object> sMembers(String key);
/**
* 向Set结构中添加属性
*/
Long sAdd(String key, Object... values);
/**
* 向Set结构中添加属性
*/
Long sAdd(String key, long time, Object... values);
/**
* 是否为Set中的属性
*/
Boolean sIsMember(String key, Object value);
/**
* 获取Set结构的长度
*/
Long sSize(String key);
/**
* 删除Set结构中的属性
*/
Long sRemove(String key, Object... values);
/**
* 获取List结构中的属性
*/
List<Object> lRange(String key, long start, long end);
/**
* 获取List结构的长度
*/
Long lSize(String key);
/**
* 根据索引获取List中的属性
*/
Object lIndex(String key, long index);
/**
* 向List结构中添加属性
*/
Long lPush(String key, Object value);
/**
* 向List结构中添加属性
*/
Long lPush(String key, Object value, long time);
/**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Object... values);
/**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Long time, Object... values);
/**
* 从List结构中移除属性
*/
Long lRemove(String key, long count, Object value);
/**
* 获取数量
* @param keyPrefix
* @return
*/
int countKey(String keyPrefix);
}
第二步,增加 RedisServiceImpl 实现类:
java
package com.codingmore.service.impl;
import com.codingmore.service.IRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* redis操作实现类
*/
@Service
public class RedisServiceImpl implements IRedisService {
@Autowired
private RedisTemplate<String, Object> 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<String> 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<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
@Override
public Boolean hSetAll(String key, Map<String, Object> map, long time) {
redisTemplate.opsForHash().putAll(key, map);
return expire(key, time);
}
@Override
public void hSetAll(String key, Map<String, Object> 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<Object> 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<Object> 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);
}
@Override
public int countKey(String keyPrefix) {
return redisTemplate.keys(keyPrefix).size();
}
}
第三步,在标签 PostTagController 中增加 Redis 测试用接口 simpleTest :
java
@Controller
@Api(tags = "标签")
@RequestMapping("/postTag")
public class PostTagController {
@Autowired
private IPostTagService postTagService;
@Autowired
private IPostTagRelationService postTagRelationService;
@Autowired
private RedisService redisService;
@RequestMapping(value = "/simpleTest", method = RequestMethod.POST)
@ResponseBody
@ApiOperation("修改标签/Redis 测试用")
public ResultObject<PostTag> simpleTest(@Valid PostTagParam postAddTagParam) {
if (postAddTagParam.getPostTagId() == null) {
return ResultObject.failed("标签id不能为空");
}
PostTag postTag = postTagService.getById(postAddTagParam.getPostTagId());
if (postTag == null) {
return ResultObject.failed("标签不存在");
}
QueryWrapper<PostTag> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("description", postAddTagParam.getDescription());
int count = postTagService.count(queryWrapper);
if (count > 0) {
return ResultObject.failed("标签名称已存在");
}
BeanUtils.copyProperties(postAddTagParam, postTag);
boolean successFlag = postTagService.updateById(postTag);
String key = "redis:simple:" + postTag.getPostTagId();
redisService.set(key, postTag);
PostTag cachePostTag = (PostTag) redisService.get(key);
return ResultObject.success(cachePostTag);
}
}
第四步 ,重启服务,使用 Knife4j 测试该接口 :