前言
学完 Redis 的基本操作命令后,就可以学习如何使用编程语言 Java 去操作 Redis 数据库(我们使用实现了 JDBC 规范的框架去操作 MySQL 数据库)。打开 Redis 官网,找到对应的 Java 客户端, 可以看到从上往下依次是 Jedis、Lettuce、Redisson...
Jedis
Redis 官网对其的描述:A blazingly small and sane Redis Java client,意为:一个非常小且稳健的 Redis Java 客户端。那么按照它的使用说明文档来测试一下。
引入依赖
Maven 项目中在 pom.xml
文件添加以下依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.0.1</version>
</dependency>
连接池
我们很少直接使用 Jedis,而是要用到 Jedis 的连接池 —— JedisPool。因为 Jedis 对象并不是线程安全的,当我们要使用 Jedis 对象时,需要从连接池中拿出一个 Jedis 对象独占,使用完毕后再将这个对象还给连接池。
你可以使用 try-catch-finally 代码块处理,在 finally 块中关闭连接,归还资源。
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(fileName));
// 执行操作
writer.write(str);
} catch (IOException e) {
// 捕获异常
} finally {
// finally 块中释放资源
try {
if (writer != null)
writer.close();
} catch (IOException e) {
// 捕获异常
}
}
这里只是举例子,不探讨 finally 块中的代码是否一定执行。
try-with-resources 的语法几乎与 try-catch-finally 的语法相同。唯一的区别是括号后的 try,我们在其中声明将使用的资源:
try(BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
// 执行操作
writer.write(str);
}catch(IOException e){
// 捕获异常
}
可以理解为:在 try 块中完成用户操作后,会立即调用它们的 .close()
方法,关闭资源是自动完成的。所以,Jedis 的文档示例可以这么使用:
try (Jedis jedis = pool.getResource()) {
jedis.set("clientName", "Jedis");
}
命令测试
这里测试连接的是 Linux 端的 Redis,需要注意的是:JedisPool 有多个构造方法,我这里使用的只是其中一个。
public JedisPool(String host, int port, String user, String password) {
this(new GenericObjectPoolConfig(), host, port, user, password);
}
如果出现了“WRONGPASS invalid username-password pair or user is disabled.”这个错误,那么检查一下用户名和密码是否正确,默认的用户名不是 root
,而是 default
!!!
@SpringBootTest
public class RedisDemoApplicationTest {
/**
* 测试 Redis 的链接
*
* @author yyt
* @date 2022/1/22 20:24
*/
@Test
void setup() {
// user 不是 root,而是 default
JedisPool pool = new JedisPool("xxx.xx.xxx.xxx", 6379, "default", "******");
// Jedis 连接池,用完会自动 close
try (Jedis jedis = pool.getResource()) {
// 如果 Redis 服务连接需要密码,就设置密码
jedis.auth("******");
if ("PONG".equals(jedis.ping())) {
System.out.println("Redis 连接成功!!!");
// 操作字符串
testStringCommands(jedis);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端 Jedis 的方法名和 Redis 的操作命令基本一致,我这里以操作 String 类型为例:
public void testStringCommands(Jedis jedis) throws InterruptedException {
System.out.println("清空当前使用的数据库:" + jedis.flushDB());
System.out.println("【SET key value】新增键值对:" + jedis.set("year", "2021"));
System.out.println("新增<'month','12'>的键值对:" + jedis.set("month", "12"));
System.out.println("新增<'newyear','2022'>的键值对:" + jedis.set("newyear", "2022"));
System.out.println("新增<'clientName','Jedis'>的键值对:" + jedis.set("clientName", "Jedis"));
System.out.println("新增<'sentence','xxx'>的键值对:"
+ jedis.set("sentence", "The quick brown fox jumps over a lazy dog"));
System.out.println("【MSET key value】批量设置键值对:"
+ jedis.mset("key1", "mytext", "key2", "mynewtext"));
// 前提是 value 可以正确转换为 64 位有符号 integer 类型
System.out.println("【DECR key】对指定 key 自减 1:" + jedis.decr("year"));
// 如果不指定步长 increment,就从 0 开始
System.out.println("【DECRBY key decrement】对指定 key 减去 decrement:"
+ jedis.decrBy("year", 20));
System.out.println("【INCR key】对指定 key 自增 1:" + jedis.incr("month"));
System.out.println("【INCRBY key increment】对指定 key 增加 increment:"
+ jedis.incrBy("month", 100));
// redis 没有 double 类型,实际上是将字符串转为双精度的数字,计算之后转为字符串类型进行赋值
System.out.println("【INCRBYFLOAT key increment】对指定 key 增加浮点数 increment:"
+ jedis.incrByFloat("month", 10.24));
// 前提是 key 是存在的,而且 value 是 String 类型
// windows 本地的 redis 的好像不支持这个命令,从 6.2.0 开始新增的
System.out.println("【GETDEL key】获取 key 的值后,就删除 key:" + jedis.getDel("year"));
// GetExParams params 对 key 设置过期时间:秒,毫秒,微妙,纳秒
System.out.println("【GETEX key】获取 key 的值,并选择设置其过期时间:"
+ jedis.getEx("newyear", new GetExParams().exAt(60L)));
System.out.println("查看键 newyear 的剩余过期时间:" + jedis.ttl("newyear"));
System.out.println("【GETRANGE key start end】获取 key 从 start 到 end 区间的字符串:"
+ jedis.getrange("sentence", 10, 18));
System.out.println("判断某个键是否存在:" + jedis.exists("clientName"));
System.out.println("【GET key】获取指定 key 的值:" + jedis.get("clientName"));
System.out.println("【MGET key】批量获取键值对:" + jedis.mget("key1", "key2"));
// 注意,这个算法不同于最长公共字符串算法,因为它匹配的部分不需要是相邻的
// strAlgoLCSKeys 返回的是一个 LCSMatchResult 类型对象
System.out.println("【LCS key1 key2】最长的公共子序列算法:"
+ jedis.strAlgoLCSKeys("key1", "key2", new StrAlgoLCSParams().len()).getLen());
// 如果 key 不存在,则效果等同于 set(key, value)
System.out.println("【APPEND key value】在指定 key 末尾拼接 value:"
+ jedis.append("clientName", "NB"));
// 当存在一个相同的 key,所有的键值对都修改失败,返回 1 说明设置成功,返回 0 说明未设置成功
System.out.println("【MSETNX key value】将给定的 key 设置各自的值:"
+ jedis.msetnx("key3", "Hello", "key4", "World"));
System.out.println("【PSETEX key milliseconds value】对 key 设置毫秒级别的有效时间:"
+ jedis.psetex("key5", 1000, "todo"));
System.out.println("【SETRANGE key offset value】覆盖 key 原来的 value,覆盖的位置从 offset 开始:"
+ jedis.setrange("sentence", 0, "现在是2022年1月18日 22:17:38"));
System.out.println("【STRLEN key】返回 key 的值的长度:" + jedis.strlen("sentence"));
System.out.println("-----以下是过期弃用的方法-----");
// 注意,从 6.2.0 开始,此命令被视为已弃用
System.out.println("【GETSET key value】获取旧的 value,并用新的 value 进行覆盖:"
+ jedis.getSet("month", "1"));
// 注意,从 2.0.0 开始,此命令被视为已弃用,使用 GETRANGE 命令代替
System.out.println("【SUBSTR key start end】返回 key 从 start 到 end 区间的字符串:" +
jedis.substr("sentence", 0, 12));
}
测试执行事务:
public void testTransaction(Jedis jedis) throws InterruptedException {
// 开启事务
Transaction transaction = jedis.multi();
// 事务操作命令
try {
transaction.set("in", "6000");
transaction.set("out", "0");
transaction.decrBy("in", 2100L);
transaction.incrBy("out", 2100L);
// 执行事务
transaction.exec();
} catch (Exception e) {
// 抛出异常就说明事务执行失败,就放弃
transaction.discard();
e.printStackTrace();
} finally {
System.out.println("【测试事务】:" + jedis.get("in"));
System.out.println("【测试事务】:" + jedis.get("out"));
}
}
Lettuce
Spring Boot 框架中已经集成了 Redis,在 1.X 版本时默认使用的 Jedis 客户端,现在是 2.X 版本默认使用的 Lettuce 客户端。
在 Spring Boot 项目的 pom.xml
文件中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
按住 Ctrl
键单击 spring-boot-starter-data-redis
这个依赖,就会发现它包含了 Spring Data Redis
和 Lettuce
等关键依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.6.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.5.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
区别
因为 Jedis 的缺点很明显:
- 使用阻塞的 I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行,不支持异步;
- Jedis 是直连模式,在多个线程间共享一个 Jedis 实例时是线程不安全的,于是需要维护一个连接池,每个线程需要时从连接池取出连接实例,完成操作后或者遇到异常归还实例。当连接数随着业务不断上升时,对物理连接的消耗也会成为性能和稳定性的潜在风险点。
而 Lettuce 的 API 是线程安全的,底层基于 Netty,连接实例可以在多个线程间共享,还支持异步模式。如果不是执行阻塞和事务操作,如 BLPOP、MULTI 或 EXEC,多个线程就可以共享一个连接。
Spring Boot 框架默认使用 Lettuce,就说明它对比 Jedis 有独到之处。看下 Spring Data Redis 帮助文档给出的对比表格:
这么一对比,Jedis 支持的 Lettuce 都支持甚至更多;Jedis 不支持的 Lettuce 也支持。其中 X 标记的表示支持的意思,不是错误的意思。
配置属性
如下图所示,在名为 org.springframework.boot:spring-boot-autoconfigure
的 JAR 文件中,打开 META-INF 下自动配置的列表 spring.factories
,使用组合键 Ctrl
+ F
找到 Spring Data Redis
的自动配置类 RedisAutoConfiguration
,按住 Ctrl
键单击它就会跳转到对应的 RedisAutoConfiguration.class 文件。
RedisAutoConfiguration 自身主要的作用就是确保需要的 Bean 都在容器中,所以通过使用 @ConditionalOnMissingBean
注解配置了默认的 Bean:RedisTemplate 和 StringRedisTemplate。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
// 默认提供一个 RedisTemplate
@Bean
@ConditionalOnMissingBean(name = {"redisTemplate"})
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
// 因为 String 类型很常用,所以单独提供一个 StringRedisTemplate
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
注意 @EnableConfigurationProperties
注解自动映射一个 POJO 到 Spring Boot 配置文件的属性集,给自动配置提供具体的配置参数,那么打开 RedisProperties.class 文件,可以看到配置的前缀名是 spring.redis
,可配置的属性有 database、url、host、username 等等,这些都可以在 application.properties
配置文件中体现。
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
private int database = 0;
private String url;
private String host = "localhost";
private String username;
private String password;
private int port = 6379;
private boolean ssl;
private Duration timeout;
private Duration connectTimeout;
private String clientName;
private RedisProperties.ClientType clientType;
private RedisProperties.Sentinel sentinel;
private RedisProperties.Cluster cluster;
private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
...
}
设置配置文件:当然你也可以使用 RedisURI 来连接。
spring:
# 配置 Redis
redis:
# 默认使用的数据库
database: 0
# IP
host: xxx.xx.xxx.xxx
# 用户名
username: default
# 密码
password: ******
# 端口号
port: 6379
# 不用 Jedis pool,使用 Lettuce pool
lettuce:
pool:
enabled: true
命令测试
为了简单(偷懒),这里只是简单测试一下基础数据类型,那些同步、异步、响应式的 API 的使用延后吧。
@SpringBootTest
public class RedisSpringBootTest {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Test
void contextLoaded() {
// 获取 redis 连接对象
RedisConnection connection = Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection();
// 清空数据
connection.flushDb();
connection.flushDb();
// 测试操作不同的数据类型
StrTest();
// 关闭连接
connection.close();
}
// 以 opsFor 为开头(OperationsFor),表示操作 String 类型数据
void StrTest() {
// set(K key, V value) 设置键值对
redisTemplate.opsForValue().set("ClientName", "Lettuce");
System.out.println(redisTemplate.opsForValue().get("ClientName"));
}
// 操作 List 类型数据
void ListTest() {
// leftPush(K key, V value) 在集合左边添加元素值
redisTemplate.opsForList().leftPush("listKey1", "value1");
}
// 操作 Hash 类型数据
void HashTest() {
// put() 添加值
redisTemplate.opsForHash().put("myhash", "field1", "value1");
}
// 操作 Set 类型数据
void SetTest() {
// add() 添加值
redisTemplate.opsForSet().add("myset", "Hello", "World", "2022");
}
// 操作 ZSet 类型数据
void ZSetTest() {
// add() 添加值
redisTemplate.opsForZSet().add("myzset", "996", 100.0);
}
}
参考
Jedis 文档
Lettuce 文档
Spring Data Redis 文档
https://q1.qlogo.cn/g?b=qq&nk=1351856278&s=100