共计 14029 个字符,预计需要花费 36 分钟才能阅读完成。
导读 | 在当前流行的微服务以及分布式集群环境下,Redis 的使用场景可以说非常的广泛,能解决集群环境下系统中遇到的不少技术问题,在此列举几个使用 redis 经常用到的功能! |
在前几篇文章中,我们详细介绍了 redis 的一些功能特性以及主流的 java 客户端 api 使用方法。
在当前流行的微服务以及分布式集群环境下,Redis 的使用场景可以说非常的广泛,能解决集群环境下系统中遇到的不少技术问题,在此列举几个使用 redis 经常用到的功能!
分布式缓存:在分布式的集群架构中,将缓存存储在内存中会出现很多的问题,比如用户回话信息,因为这部分信息需要与其他机器共享,此时利用 Redis 可以很好的解决机器之间数据共享的问题,缓存也是 Redis 中使用最多的场景
分布式锁:在高并发的情况下,我们需要一个锁来防止并发带来的脏数据,Java 自带的锁机制显然对进程间的并发并不好使,此时利用 Redis 的单线程特性,实现分布式锁控制
接口限流:在集群环境下,可以利用 redis 的分布式自增 ID 功能,精准的统计每个接口在指定时间内的请求次数,利用这个特性,可以定向限制某个接口恶意频刷
当然 Redis 的使用场景并不仅仅只有这么多,还有很多未列出的场景,如发布 / 订阅,分布锁集合等。
现实中我们大部分的微服务项目,都是基于 SpringBoot 框架进行快速开发,在 SpringBoot 项目中我们应该如何使用 Redis 呢?
代码实践如下!
实际上,在 SpringBoot 项目中,使用 redis 非常简单,开发者只需要在项目中添加如下的依赖即可!
org.springframework.boot | |
spring-boot-starter-data-redis |
在之前的 redis 系列文章中,我们知道官方推荐的 java 版本的 redis 客户端,一共有三个,分别是 Jedis、Lettuce 和 Redisson,其中大部分场景下,使用 Jedis或者 Lettuce 就足够了。
在 SpringBoot 1.x 版本里面,spring-boot-starter-data-redis默认集成的客户端是 Jedis;从 SpringBoot 2.x 开始,spring-boot-starter-data-redis默认集成的客户端是 Lettuce。
以 springBoot-2.1.0版本为例,我们打开 spring-boot-starter-data-redis 依赖配置,核心配置如下!
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |
<modelVersion>4.0.0</modelVersion> | |
<parent> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starters</artifactId> | |
<version>2.1.0.RELEASE</version> | |
</parent> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-data-redis</artifactId> | |
<version>2.1.0.RELEASE</version> | |
<name>Spring Boot Data Redis Starter</name> | |
<!-- 省略其他配置...--> | |
<dependencies> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter</artifactId> | |
<version>2.1.0.RELEASE</version> | |
<scope>compile</scope> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.data</groupId> | |
<artifactId>spring-data-redis</artifactId> | |
<version>2.1.2.RELEASE</version> | |
<scope>compile</scope> | |
<exclusions> | |
<exclusion> | |
<artifactId>jcl-over-slf4j</artifactId> | |
<groupId>org.slf4j</groupId> | |
</exclusion> | |
</exclusions> | |
</dependency> | |
<dependency> | |
<groupId>io.lettuce</groupId> | |
<artifactId>lettuce-core</artifactId> | |
<version>5.1.2.RELEASE</version> | |
<scope>compile</scope> | |
</dependency> | |
</dependencies> | |
</project> |
可以很清晰的看到,spring-boot-starter-data-redis默认集成的客户端是 Lettuce。
依赖包添加完成之后,我们还需要在 application.properties 全局配置文件中,添加相关的 redis 配置信息。
# Redis 数据库索引(默认为 0)spring.redis.database=0 | |
# Redis 服务器地址 | |
spring.redis.host=127.0.0.1 | |
# Redis 服务器连接端口 | |
spring.redis.port=6379 | |
# Redis 服务器连接密码(默认为空)spring.redis.password= | |
# 连接池最大连接数(使用负值表示没有限制)默认 8 | |
spring.redis.lettuce.pool.max-active=8 | |
# 连接池最大阻塞等待时间(使用负值表示没有限制)默认 -1 | |
spring.redis.lettuce.pool.max-wait=-1 | |
# 连接池中的最大空闲连接 默认 8 | |
spring.redis.lettuce.pool.max-idle=8 | |
# 连接池中的最小空闲连接 默认 0 | |
spring.redis.lettuce.pool.min-idle=0 |
最后,我们来跑一个最简单的单元测试,看看是否能联通(确保 redis 的服务端已经启动)。
private StringRedisTemplate stringRedisTemplate; | |
public void test() throws Exception {String uuid = UUID.randomUUID().toString(); | |
stringRedisTemplate.opsForValue().set(uuid, uuid, 60, TimeUnit.SECONDS); | |
System.out.println(stringRedisTemplate.opsForValue().get(uuid)); | |
} |
如果控制台输出正常,说明基本配置已经完成,如果有错误,看错误信息然后依次排查!
SpringBoot 为我们提供了一个高度封装的 RedisTemplate类来操作 redis的各个命令,开发者无需关心具体的客户端 api 问题,通过 RedisTemplate提供的方法,就可以操作 redis,方便开发者可以无成本替换 java 客户端。
当我们存储对象的时候,RedisTemplate 默认采用的是 Jdk 提供的序列化工具库,该工具有个要求,缓存的对象必须显式实现序列化接口,才能保存。
通常情况下,我们会自定义 RedisTemplate的序列化策略,采用 Jackson将对象转成 json,查询的时候将 json 转成对象。
具体实现如下:
@Configuration | |
public class RedisTemplateConfig { | |
/** | |
* 默认是 JDK 的序列化策略,这里配置 redisTemplate 采用的是 Jackson2JsonRedisSerializer 的序列化策略 | |
* @param factory | |
* @return | |
*/ | |
@Bean | |
public RedisTemplate redisTemplate(RedisConnectionFactory factory){ | |
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值(默认使用 JDK 的序列化方式)Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); | |
// 使用 Jackson 序列号对象 | |
ObjectMapper objectMapper = new ObjectMapper(); | |
// 指定要序列化的域,field,get 和 set, 以及修饰符范围,ANY 是都有包括 private 和 public | |
objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY); | |
// 指定序列化输入的类型,类必须是非 final 修饰的,final 修饰的类,比如 String,Integer 等会抛出异常 | |
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); | |
jacksonSeial.setObjectMapper(objectMapper); | |
// 使用 RedisTemplate 对象 | |
RedisTemplate template = new RedisTemplate(); | |
// 配置连接工厂 | |
template.setConnectionFactory(factory); | |
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值 | |
template.setKeySerializer(new StringRedisSerializer()); | |
// 值采用 json 序列化 | |
template.setValueSerializer(jacksonSeial); | |
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 hash-key 值 | |
template.setHashKeySerializer(new StringRedisSerializer()); | |
// 值采用 json 序列化 | |
template.setHashValueSerializer(jacksonSeial); | |
// 执行后续方法 | |
template.afterPropertiesSet(); | |
return template; | |
} | |
} |
我们知道,redis 提供的数据结构很丰富,支持字符串、哈希表、列表、集合、有序集合等数据类型的存储,RedisTemplate 对这五种数据结构分别定义了不同的操作类,具体如下:
相关的 api 操作如下!
public class RedisTest { | |
private StringRedisTemplate stringRedisTemplate; | |
public void test() throws Exception { | |
// 设置值,默认不过期 | |
stringRedisTemplate.opsForValue().set("userName", "张三"); | |
// 获取值 | |
String value = stringRedisTemplate.opsForValue().get("userName"); | |
System.out.println("获取 userName 对应的值:" + value); | |
// 设置值并且设置 2 秒过期时间,过期之后自动删除 | |
stringRedisTemplate.opsForValue().set("email", "123@123.com", 2, TimeUnit.SECONDS); | |
Thread.sleep(1000); | |
System.out.println("获取 email 过期时间(单位秒):" + stringRedisTemplate.getExpire("email")); | |
System.out.println("获取 email 对应的值:" + stringRedisTemplate.opsForValue().get("email")); | |
Thread.sleep(1000); | |
System.out.println("获取 email 对应的值:" + stringRedisTemplate.opsForValue().get("email")); | |
// 删除 key | |
Boolean result = stringRedisTemplate.delete("userName"); | |
System.out.println("删除 userName 结果:" + result); | |
} | |
} |
public class UserVo { | |
private String email; | |
private String name; | |
public String getEmail(){return email;} | |
public void setEmail(String email){this.email = email;} | |
public String getName(){return name;} | |
public void setName(String name){this.name = name;} | |
public UserVo(String email, String name){ | |
this.email = email; | |
this.name = name; | |
} | |
public UserVo(){} | |
public String toString(){ | |
return "UserVo{" + | |
"email='" + email + '\'' + | |
", name='" + name + '\'' + | |
'}'; | |
} | |
} | |
private RedisTemplate redisTemplate; | |
public void test() throws Exception { | |
// 设置对象值,并且 2 秒自动过期 | |
ValueOperations operations = redisTemplate.opsForValue(); | |
UserVo user = new UserVo("aa@126.com", "张三"); | |
operations.set("user", user, 2, TimeUnit.SECONDS); | |
// 获取对象值 | |
UserVo userVo = operations.get("user"); | |
System.out.println(userVo.toString()); | |
System.out.println("获取 user 过期时间(单位秒):" + redisTemplate.getExpire("user")); | |
// 删除 key | |
Boolean deleteValue = redisTemplate.delete("user"); | |
System.out.println("删除 userName 结果:" + deleteValue); | |
} |
private RedisTemplate redisTemplate; | |
public void test() throws Exception { | |
// 向列表中添加数据 | |
ListOperations operations = redisTemplate.opsForList(); | |
// 往 List 左侧插入一个元素 | |
operations.leftPush("userList", new UserVo("aa@126.com", "张三")); | |
operations.leftPush("userList", new UserVo("bb@126.com", "里斯")); | |
// 往 List 右侧插入一个元素 | |
operations.rightPush("userList", new UserVo("cc@126.com", "王五")); | |
operations.rightPush("userList", new UserVo("dd@126.com", "赵六")); | |
// 获取 List 大小 | |
Long size = operations.size("userList"); | |
System.out.println("获取列表总数:" + size); | |
// 遍历整个 List | |
List allUserVo1 = operations.range("userList", 0, size); | |
System.out.println("遍历列表所有数据:" + JacksonUtils.toJson(allUserVo1)); | |
// 遍历整个 List,- 1 表示倒数第一个即最后一个 | |
List allUserVo2 = operations.range("userList", 0, -1); | |
System.out.println("遍历列表所有数据:" + JacksonUtils.toJson(allUserVo2)); | |
// 从 List 左侧取出第一个元素,并移除 | |
Object userVo1 = operations.leftPop("userList", 200, TimeUnit.MILLISECONDS); | |
System.out.println("从左侧取出第一个元素并移除:" + userVo1.toString()); | |
// 从 List 右侧取出第一个元素,并移除 | |
Object userVo2 = operations.rightPop("userList", 200, TimeUnit.MILLISECONDS); | |
System.out.println("从右侧取出第一个元素并移除:" + userVo2.toString()); | |
} |
private RedisTemplate redisTemplate; | |
public void test() throws Exception { | |
// 向 hash 中添加数据 | |
HashOperations operations = redisTemplate.opsForHash(); | |
//Hash 中新增元素。operations.put("score", "张三", 2); | |
operations.put("score", "里斯", 1); | |
operations.put("score", "王五", 3); | |
operations.put("score", "赵六", 4); | |
Boolean hasKey = operations.hasKey("score", "张三"); | |
System.out.println("检查是否存在【score】【张三】:" + hasKey); | |
Integer value = operations.get("score", "张三"); | |
System.out.println("获取【score】【张三】的值:" + value); | |
Set keys = operations.keys("score"); | |
System.out.println("获取 hash 表【score】所有的 key 集合:" + JacksonUtils.toJson(keys)); | |
List values = operations.values("score"); | |
System.out.println("获取 hash 表【score】所有的 value 集合:" + JacksonUtils.toJson(values)); | |
Map map = operations.entries("score"); | |
System.out.println("获取 hash 表【score】下的 map 数据:" + JacksonUtils.toJson(map)); | |
Long delete = operations.delete("score", "里斯"); | |
System.out.println("删除【score】中 key 为【里斯】的数据:" + delete); | |
Boolean result = redisTemplate.delete("score"); | |
System.out.println("删除整个 key:" + result); | |
} |
@Autowired | |
private RedisTemplate redisTemplate; | |
@Test | |
public void test() throws Exception { | |
// 向集合中添加数据 | |
SetOperations operations = redisTemplate.opsForSet(); | |
// 向集合中添加元素,set 元素具有唯一性 | |
operations.add("city", "北京","上海", "广州", "深圳", "武汉"); | |
Long size = operations.size("city"); | |
System.out.println("获取集合总数:" + size); | |
// 判断是否是集合中的元素 | |
Boolean isMember = operations.isMember("city", "广州"); | |
System.out.println("检查集合中是否存在指定元素:" + isMember); | |
Set cityNames = operations.members("city"); | |
System.out.println("获取集合所有元素:" + JacksonUtils.toJson(cityNames)); | |
Long remove = operations.remove("city", "广州"); | |
System.out.println("删除指定元素结果:" + remove); | |
// 移除并返回集合中的一个随机元素 | |
String cityName = operations.pop("city"); | |
System.out.println("移除并返回集合中的一个随机元素:" + cityName); | |
} |
@Autowired | |
private RedisTemplate redisTemplate; | |
@Test | |
public void test() throws Exception { | |
// 向有序集合中添加数据 | |
ZSetOperations operations = redisTemplate.opsForZSet(); | |
// 向有序集合中添加元素,set 元素具有唯一性 | |
operations.add("cityName", "北京", 100); | |
operations.add("cityName", "上海", 95); | |
operations.add("cityName", "广州", 75); | |
operations.add("cityName", "深圳", 85); | |
operations.add("cityName", "武汉", 70); | |
// 获取变量指定区间的元素。0, - 1 表示全部 | |
Set ranges = operations.range("cityName", 0, -1); | |
System.out.println("获取有序集合所有元素:" + JacksonUtils.toJson(ranges)); | |
Set byScores = operations.rangeByScore("cityName", 85, 100); | |
System.out.println("获取有序集合所有元素(按分数从小到大):"+ JacksonUtils.toJson(byScores)); | |
Long zCard = operations.zCard("cityName"); | |
System.out.println("获取有序集合成员数:" + zCard); | |
Long remove = operations.remove("cityName", "武汉"); | |
System.out.println("删除某个成员数结果:" + remove); | |
} |
private StringRedisTemplate stringRedisTemplate; | |
public void test(){ | |
// 分布式自增 ID | |
for (int i = 0; i redisScript = new DefaultRedisScript(luaScript, Long.class); | |
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(expire)); | |
return result.equals(Long.valueOf(1)); | |
} | |
/** | |
* 释放锁 | |
* @param key | |
* @param value | |
* @return | |
*/ | |
public boolean releaseLock(String key,String value){String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; | |
RedisScript redisScript = new DefaultRedisScript(luaScript, Long.class); | |
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key),value); | |
return result.equals(Long.valueOf(1)); | |
} |
从 SpringBoot 2.x 开始,spring-boot-starter-data-redis默认集成的客户端是 Lettuce,但是有的项目使用了 Jedis 依赖包相关的代码,如何无缝替换呢?
在 pom.xml文件中,添加 Jedis 依赖包,排除相关的包即可,示例代码如下:
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-data-redis</artifactId> | |
<exclusions> | |
<exclusion> | |
<groupId>redis.clients</groupId> | |
<artifactId>jedis</artifactId> | |
</exclusion> | |
<exclusion> | |
<artifactId>lettuce-core</artifactId> | |
<groupId>io.lettuce</groupId> | |
</exclusion> | |
</exclusions> | |
</dependency> | |
<dependency> | |
<groupId>redis.clients</groupId> | |
<artifactId>jedis</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.apache.commons</groupId> | |
<artifactId>commons-pool2</artifactId> | |
</dependency> |
最后,在 application.properties中,添加 jedis 相关配置,内容如下:
# Redis 数据库索引(默认为 0)spring.redis.database=1 | |
# Redis 服务器地址 | |
spring.redis.host=127.0.0.1 | |
# Redis 服务器连接端口 | |
spring.redis.port=6379 | |
# Redis 服务器连接密码(默认为空)spring.redis.password= | |
# Redis 服务器连接超时配置 | |
spring.redis.timeout=1000 | |
# 连接池配置 | |
spring.redis.jedis.pool.max-active=8 | |
spring.redis.jedis.pool.max-wait=1000 | |
spring.redis.jedis.pool.max-idle=8 | |
spring.redis.jedis.pool.min-idle=0 | |
spring.redis.jedis.pool.time-between-eviction-runs=100 |
默认情况下,无论是 Jedis还是 Lettuce,都没有为我们提供 redis分布式锁的实现,因此我们自己进行封装,当然你也可以直接添加 Redisson 包,里面也提供了分布式锁实现的相关 API。
如果当前 redis 是单机环境,或者哨兵模式,我们完全可以自行封装一个分布式锁实现类,具体代码如下:
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.data.redis.core.StringRedisTemplate; | |
import org.springframework.data.redis.core.script.DefaultRedisScript; | |
import org.springframework.data.redis.core.script.RedisScript; | |
import org.springframework.stereotype.Component; | |
import java.time.Duration; | |
import java.util.Collections; | |
/** | |
* redis 分布式锁服务类 | |
* 采用 LUA 脚本实现,保证加锁、解锁操作原子性 | |
*/ | |
public class RedisLockService { | |
/** | |
* 分布式锁过期时间,单位秒 | |
*/ | |
private static final Long DEFAULT_LOCK_EXPIRE_TIME = 60L; | |
private StringRedisTemplate stringRedisTemplate; | |
/** | |
* 尝试在指定时间内加锁 | |
* @param key | |
* @param value | |
* @param timeout 锁等待时间 | |
* @return | |
*/ | |
public boolean tryLock(String key,String value, Duration timeout){long waitMills = timeout.toMillis(); | |
long currentTimeMillis = System.currentTimeMillis(); | |
do {boolean lock = lock(key, value, DEFAULT_LOCK_EXPIRE_TIME); | |
if (lock) {return true;} | |
try {Thread.sleep(1L); | |
} catch (InterruptedException e) {Thread.interrupted(); | |
} | |
} while (System.currentTimeMillis() redisScript = new DefaultRedisScript(luaScript, Long.class); | |
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(expire)); | |
return result.equals(Long.valueOf(1)); | |
} | |
/** | |
* 释放锁 | |
* @param key | |
* @param value | |
* @return | |
*/ | |
public boolean releaseLock(String key,String value){String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; | |
RedisScript redisScript = new DefaultRedisScript(luaScript, Long.class); | |
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key),value); | |
return result.equals(Long.valueOf(1)); | |
} | |
} |
通常情况下,我们对 key采用如下方式进行设计,以便与其他项目中的 key 错开,避免发生冲突!
固定前缀: 项目名: 数据库名: 表名: 字段名: 具体的值
其次,无论什么时候,只要有可能就利用 key超时的优势,尽可能避免数据永久存储,因为一旦所有的 key都永久存储,大量无效的 key,会服务器资源非常严重不足,甚至不可用!
本文主要围绕在 SpringBoot 项目中,如何集成 redis 并正确使用进行了简单的分享,内容难免有缺漏,欢迎网友留言指出!
1、博客园 – 卡斯特梅的雨伞 – springboot 中 RedisTemplate 的使用
