SpringBoot项目中应用Jedis和一些常见配置
优雅的使用Jedis
博客地址:https://www.cnblogs.com/keatsCoder/p/12609109.html 转载请注明出处,谢谢
Redis的Java客户端有很多,Jedis是其中使用比较广泛和性能比较稳定的一个。并且其API和RedisAPI命名风格类似,推荐大家使用
在项目中引入Jedis
可以通过Maven的方式直接引入,目前最新版本是3.2.0
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
直连及使用连接池
Jedis直连
引入Jedis之后,项目可以通过 new 的方式获取 Jedis 使用。
- 首先在yml中配置好 redis 的地址和端口
@SpringBootTest(classes = RedisCliApplication.class)
@RunWith(SpringRunner.class)
public class JedisConnectionDemo {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Test
public void testConnection(){
// 建立连接
Jedis jedis = new Jedis(host, port);
// 添加 key-value。添加成功则返回OK
String setResult = jedis.set("name", "keats");
Assert.assertEquals("OK", setResult);
// 通过 key 获取 value
String value = jedis.get("name");
Assert.assertEquals("keats", value);
// 关闭连接
jedis.close();
}
}
使用连接池
直连的话每次都会新建TCP连接和断开TCP连接,这个过程是很耗时的,对于Redis这种需要频繁访问和高效访问的软件显然是不合适的。并且也不方便对连接进行管理。类似数据库连接池思想,Jedis也提供了JedisPool连接池进行连接池管理。所有的Jedis对象预先放在JedisPool中,客户端需要使用的时候从池中借用,用完后归还到池中。这样避免了频繁建立和断开TCP连接的网络开销,速度非常快。并且通过合理的配置也能实现合理的管理连接,分配连接。
@Test
public void testConnectionWithPool(){
// 创建连接池
JedisPool jedisPool = new JedisPool(host, port);
Jedis jedis = jedisPool.getResource();
// doSomething
// 归还连接
jedis.close();
}
这里虽然最后使用的 close() 方法,字面意思看起来好像是关闭连接,实际上点进去可以发现,如果dataSource(连接池)不为空,将执行归还连接的方法
@Override
public void close() {
if (dataSource != null) {
if (client.isBroken()) {
this.dataSource.returnBrokenResource(this);
} else {
this.dataSource.returnResource(this);
}
} else {
client.close();
}
}
连接池使用的一个常见问题
上面归还连接的方法有没有问题呢?试想一下,如果在执行任务的时候,报了异常,那么势必是不能执行 close() 方法的,久而久之池中的 Jedis 连接就会耗尽,整个服务可能就不能在使用了。这个问题在开发和测试环境下一般不容易发现,而生产环境由于使用量增多,就会暴露出来。
JedisPool中默认的最大连接数是8个,默认的从池中获取连接超时时间是 -1(表示一直等待)
为了演示不归还连接产生的错误,我写了下面的代码
@Test
public void testConnectionNotClose(){
// 创建连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxWaitMillis(5000L); // 等待Jedis连接超时时间
JedisPool jedisPool = new JedisPool(poolConfig, host, port);
try {
for (int i = 1; i <= 10; i++) {
Jedis jedis = jedisPool.getResource();
System.out.println(i);
// doSomething
}
} catch (Exception e) {
e.printStackTrace();
}
}
循环前8次,分别从池中获取一个连接进行使用而不归还。第9次的时候想要获取连接已经没有了。默认情况下会一直等待。而我更改了配置是5S,等待5S就会报错,错误信息如下:
1
2
3
4
5
6
7
8
redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
at redis.clients.util.Pool.getResource(Pool.java:51)
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:99)
at cn.keats.rediscli.jedis.JedisConnectionDemo.testConnectionNotClose(JedisConnectionDemo.java:64)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:439)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:349)
at redis.clients.util.Pool.getResource(Pool.java:49)
... 32 more
无论是报错还是一直等待,这在生产环境中无异于宕机。所以这个操作一定是要避免掉的。那么我在执行代码的最后一句写上 close() 是不是就高枕无忧了呢?认真从前面都过来的同学肯定会说不是的。因为当代码一旦抛出异常。是不能执行到 close() 方法的。
@Test
public void testConnectionWithException() {
// 创建连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxWaitMillis(5000L); // 等待Jedis连接超时时间
JedisPool jedisPool = new JedisPool(poolConfig, host, port);
for (int i = 1; i <= 8; i++) {
System.out.println(i);
try {
new Thread(() -> {
Jedis jedis = jedisPool.getResource();
// doSomething
// 模拟一个错误
int j = 1 / 0;
jedis.close();
}).run();
} catch (Exception e) {
// 服务器运行过程中出现了8次异常,没有执行到close方法
}
}
// 第9次无法获取连接
Jedis jedis = jedisPool.getResource();
}
这样还会报和上面一样的错误。推荐使用 Java7 之后的 try with resources 写法来完成连接归还。
try (Jedis jedis = jedisPool.getResource()) {
new Thread(() -> {
// doSomething
// 模拟一个错误
int j = 1 / 0;
jedis.close();
}).run();
} catch (Exception e) {
// 异常处理
}
这样相当于写了 finally。在正常执行/出错时都会执行 close() 方法关闭连接。除非代码中写了死循环。
这样写还有一个弊端就是有的小伙伴可能忘记归还,《Redis深度历险:核心原理和应用实践》作者老钱介绍了一种强制归还的连接池管理办法:
通过一个特殊的自定义的 RedisPool 对象将 JedisPool 对象隐藏起来,避免程序员直接使用它的 getResource 方法而忘记了归还。程序员使用 RedisPool 对象时需要提供一个
回调类来才能使用 Jedis 对象。结合 Java8 的 Lambda 表达式。使用起来也还可以。但是因此产生了闭包的问题,Lambda中的匿名内部类无法访问外部的变量。他又采用了 Hodler 来将变量包装以达到其被访问的目的。大佬的方法很厉害。但是个人愚见,这样代码的复杂度提高了很多。对于一个使用完Resource完后忘记归还的程序员来说写起来可能比较复杂。所以就不在博客中贴出了。感兴趣的伙伴可以读一下老钱的书或者从我的GITHUB中查阅老钱的代码:优雅的Jedis-老钱
连接池配置详解
除了使用默认构造方法初始化连接池外,Jedis还提供了配置类来初始化
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxWaitMillis(5000L); // 等待Jedis连接超时时间
JedisPool jedisPool = new JedisPool(poolConfig, host, port);
配置类常用的参数解释如下:
| 参数名 | 含义 | 默认值 |
|---|---|---|
| maxActive | 连接池中的最大连接数 | 8 |
| maxIdle(minIdle) | 连接池中的最大(小)空闲连接数 | 8(0) |
| maxWaitMillis | 当链接池没有连接时,调用者的最大等待时间,单位是毫秒。不建议使用默认值 | -1 表示一直等 |
| jmxEnabled | 是否开启jmx监控 | |
| minEvictableIdleTimeMillis | 连接的最小空闲时间,达到此值后空闲连接将被移除 | 1800000L 30分钟 |
| numTestsPerEvictionRun | 做空闲连接检测时,每次的采样数 | 3 |
| testOnBorrow | 向连接池借用连接时是否做连接有效性检测(Ping)无效连接将会被删除 | false |
| testOnReturn | 是否做周期性空闲检测 | false |
| testWhileIdle | 向连接池借用连接时是否做空闲检测,空闲超时的将会被移除 | false |
| timeBetweenEvictionRunsMillis | 空闲连接的检测周期,单位为毫秒 | -1 不做检测 |
| blockWhenExhausted | 当连接池资源耗尽时,调用者是否需要等待。和maxWaitMillis对应,当它为true时,maxWaitMillis生效 | true |
PipeLine一次执行多个命令
Redis虽然提供了 mset、mget 等方法。但是并未提供 mdel 方法。我们在业务中如果遇到一次 mget 后,有多个需要删除的 key,可以通过 PipeLine 来模拟 mdel。虽然操作不是原子性的,但大多数情况下也能满足要求:
@Test
public void testPipeline() {
// 创建连接池
JedisPool jedisPool = new JedisPool(host, port);
try (Jedis jedis = jedisPool.getResource()){
Pipeline pipelined = jedis.pipelined();
// doSomething 获取 keys
List<String> keys = new ArrayList<>();
// pipelined 添加命令
for (String key : keys) {
pipelined.del(key);
}
// 执行命令
pipelined.sync();
}
}
项目代码
在学习Redis的过程中,我将博客中的代码都在Github中上传,以便小伙伴们核对。项目地址:https://github.com/keatsCoder/redis-cli
参考文献
《Redis开发与运维》 --- 付磊 张益军
《Redis深度历险:核心原理和应用实践》 --- 钱文品
SpringBoot项目中应用Jedis和一些常见配置的更多相关文章
- 自身使用的springboot项目中比较全的pom.xml
在学习的时候常建新的项目,mark下商用的jar <dependency> <groupId>org.mybatis</groupId> <artifactI ...
- Spring-Boot项目中配置redis注解缓存
Spring-Boot项目中配置redis注解缓存 在pom中添加redis缓存支持依赖 <dependency> <groupId>org.springframework.b ...
- SpringBoot12 QueryDSL01之QueryDSL介绍、springBoot项目中集成QueryDSL
1 QueryDSL介绍 1.1 背景 QueryDSL的诞生解决了HQL查询类型安全方面的缺陷:HQL查询的扩展需要用字符串拼接的方式进行,这往往会导致代码的阅读困难:通过字符串对域类型和属性的不安 ...
- 在SpringBoot项目中添加logback的MDC
在SpringBoot项目中添加logback的MDC 先看下MDC是什么 Mapped Diagnostic Context,用于打LOG时跟踪一个“会话“.一个”事务“.举例,有一个web ...
- springboot 项目中获取默认注入的序列化对象 ObjectMapper
在 springboot 项目中使用 @SpringBootApplication 会自动标记 @EnableAutoConfiguration 在接口中经常需要使用时间类型,Date ,如果想要格式 ...
- springboot项目中js、css静态文件路径访问
springboot静态文件访问的问题,相信大家也有遇到这个问题,如下图项目结构. 项目结构如上所示,静态页面引入js.css如下所示. 大家肯定都是这样写的,但是运行的话就是出不来效果,图片也不显示 ...
- 解决springboot项目中@Value注解参数值为null的问题
1.错误场景: springboot项目中在.properties文件(.yml)文件中配置了属性值,在Bean中使用@Value注解引入该属性,Bean的构造器中使用该属性进行初始化,此时有可能会出 ...
- springboot项目中引用其他springboot项目jar
1. 剔除要引入的springboot项目中不需要的文件:如Application和ApplicationTests等 2.打包 不能使用springboot项目自带的打包插件进行打包: 3.打包 4 ...
- 在前后端分离的SpringBoot项目中集成Shiro权限框架
参考[1].在前后端分离的SpringBoot项目中集成Shiro权限框架 参考[2]. Springboot + Vue + shiro 实现前后端分离.权限控制 以及跨域的问题也有涉及
随机推荐
- css雪碧图压缩
cssgaga下载地址 链接: https://pan.baidu.com/s/1Q9xH_XzumIc7vTLCZ3tr5A 提取码: stqe CssGaga功能特性 合并import的CSS文件 ...
- USB小白学习之路(6) IIC EEPROM读取解析
IIC EEPROM读取解析 1. 编译错误处理(这里可以忽略) 在解压包解压了程序后,直接编译,出现如下错误. *** WARNING L14: INCOMPATIBLE MEMORY MODEL ...
- 【深入理解Java虚拟机 】类加载器的命名空间以及类的卸载
类加载器的命名空间 每个类加载器又有一个命名空间,由其以及其父加载器组成 类加载器的命名空间的作用和影响 每个类加载器又有一个命名空间,由其以及其父加载器组成 在每个类加载器自己的命名空间中不能出现相 ...
- pem文件转换pub
security CRT在key登陆的时候只能使用.pub文件,所以需呀将.pem转换成.pub 生成公密钥 .pub 文件.ssh-keygen -e -f key.pem >> key ...
- node--CommonJS
1.CommonJS 1)弥补js没有标准的缺陷 2.Node模块 1)分为核心模块和用户自定义模块 2)我们可以把公共的功能抽离为一个单独的js文件作为一个模块 其中的成员和属性外界无法访问,若要设 ...
- 题解-[HNOI2001]遥控赛车比赛
题解-[HNOI2001]遥控赛车比赛 前置知识:记忆化搜索.\(\texttt{Bfs}\). 参考资料 https://www.luogu.com.cn/blog/CYJian/solution- ...
- 关于使用map存放数据乱序”问题“
今天做项目中遇到了一个比较低级的错误,如果没注意将会变的更麻烦... 其实吧,也不难,要求就是将list中的值转为map后,再顺序输出map中的值,list的顺序怎样,加入到map的顺序也应怎样,不能 ...
- 峰哥说技术:07-SpringBoot 正好Thymeleaf视图
Spring Boot深度课程系列 峰哥说技术—2020庚子年重磅推出.战胜病毒.我们在行动 07 峰哥说技术:SpringBoot 正好Thymeleaf视图 Spring Boot视图介绍 虽然 ...
- 日常破解--XCTF easy_apk
一.题目来源 来源:XCTF社区安卓题目easy_apk 二.破解思路 1.首先运行一下给的apk,发现就一个输入框和一个按钮,随便点击一下,发现弹出Toast验证失败.如下图所示: ...
- Xcode调试之exc_bad_access以及 message sent to deallocated instance
如果出现exc_bad_access错误,基本上是由于内存泄漏,错误释放,对一个已经释放的对象进行release操作.但是xcode有时候不会告诉你错误在什么地方(Visual Studio这点做得很 ...