目录
- 一、前言
- 二、基本使用
- 三、Jedis连接池
- 四、连接池参数
- 五、哨兵模式
- 六、集群模式
- 七、Springboot当中使用Jedis
- 八、Springboot源码分析
一、前言
Jedis是Redis的一款Java语言的开源客户端连接工具,什么是客户端?就是真正使用者,像我们安装Redis的时候都会有一个redis-cli
这其实就是Redis提供的一个基于操作系统(linux、windows)的客户端。也就是真正使用者是电脑,电脑通过这个客户端可以连接Redis并操作Redis。Jedis同理,他的真正使用者是Java,Java语言可以通过Jedis来连接Redis并操作Redis。
关于Redis的客户端有很多,每个编程语言都有自己的Redis客户端,可以在官网上查看目前的客户端都有哪些:https://redis.com.cn/clients.html#java
目前Redis 官网推荐使用的 Java客户端 有三款:Jedis、lettuce、Redisson
,其中Jedis是最基础的客户端,他只是将Redis的所有命令封装成了Java可直接调用的方法。比如Redis的set命令就对应了Jedis的set方法,get命令就对应了Jedis的get方法。他并没有替我们封装一些基于Redis的特殊功能,比如分布式锁等等…
优点: 它非常小巧,实现原理也很简单,最重要的是很稳定,而且使用的方法参数名称和官方的文档非常 match,如果有什么方法不会用,直接参考官方的指令文档阅读一下就会了,省去了非必要的重复学习成本。不像有些客户端把方法名称都换了,虽然表面上给读者带来了便捷,但是需要挨个重新学习这些 API,提高了学习成本。
缺点: Jedis是直连模式,在多个线程间共享一个Jedis实例时是线程不安全的。这里说的不安全不是说Redis处理数据不安全,而是Jedis向Redis服务器提交数据和从Redis上拿数据不安全,简单阅读Jedis的源码就不难发现,在单个Jedis实例中,向Redis推数据的写流(RedisOutputStream)和从Redis获取数据的读流(RedisInputStream)都是全局属性,当多个线程同时使用这个Jedis实例,也就是说同时操作Redis的写流和读流,结果显而易见,数据会发生不可描述的奇妙融合。
那Jedis是不是就不能用了呢?
不是的,多个线程用一个实例会产生问题,那我们就避免这个情况呀,我们为每一个线程分配一个Jedis实例,让他们单独去操作自己的数据。一般使用JedisPoll线程池来实现。
Jedis的Github源码:https://github.com/redis/jedis
二、基本使用
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.4.1</version>
</dependency>
安装完 Redis 的驱动程序后,接下来就是连接 Redis 服务器。最简单的连接方式,就是通过 Jedis 对象连接。代码如下:
//引入Redis驱动程序
import redis.clients.jedis.Jedis;public class RedisJava {public static void main(String[] args) {//连接本地的 Redis 服务Jedis jedis = new Jedis("localhost",6379);// 如果设置 Redis 服务的密码,需要进行验证,若没有则可以省去// jedis.auth("123456");System.out.println("连接成功");//查看服务是否运行System.out.println("服务正在运行: "+jedis.ping());//设置 redis 字符串数据jedis.set("webkey", "swawdwa");// 获取存储的数据并输出System.out.println("redis 存储的字符串为: "+ jedis.get("webkey"));}
}
同时我们可以使用Jedis 来操作Redis当中的事务。Redis 事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。我们可以通过try-catch可以让他具备原子性。
@Test
void contextLoads1() {Jedis jedis = new Jedis("127.0.0.1", 6379);// 情况数据库jedis.flushDB();// 开启事务Transaction multi = jedis.multi();// jedis.watch(result)try {multi.set("user1", "123");multi.set("user2", "456");// 代码抛出异常事务,执行失败!int i = 1 / 0;// 执行事务!multi.exec();} catch (Exception e) {// 放弃事务multi.discard();e.printStackTrace();} finally {System.out.println(jedis.get("user1"));System.out.println(jedis.get("user2"));jedis.close(); // 关闭连接}
}
同时Jedis也支持发布订阅:
/**
* 订阅消息
*
* @throws InterruptedException
*/
public class JedisDemo {Jedis jedis;@Beforepublic void before() {this.jedis = new Jedis("192.168.200.129", 6379);}@Afterpublic void after() {//关闭jedisthis.jedis.close();}@Testpublic void subscribeTest() throws InterruptedException {//subscribe(消息监听器,频道列表)jedis.subscribe(new JedisPubSub() {@Overridepublic void onMessage(String channel, String message) {System.out.println(channel + ":" + message);}}, "sitemsg");TimeUnit.HOURS.sleep(1);}@Testpublic void publishTest() {// 发布消息jedis.publish("sitemsg", "hello redis");}
}
jedi实例实现了大多数Redis命令。有关支持的命令的完整列表,请参阅jedi Javadocs:https://www.javadoc.io/doc/redis.clients/jedis/latest/redis/clients/jedis/Jedis.html
三、Jedis连接池
虽然 redis 服务端是单线程操作,但是在实际项目中,使用 Jedis 对象来操作 redis 时,每次操作都需要新建/关闭 TCP 连接,连接资源开销很高,同时 Jedis 对象的个数不受限制,在极端情况下可能会造成连接泄漏,同时 Jedis 存在多线程不安全的问题。一定要避免多个线程同时使用一个Jedis实例!
所以我们需要将 Jedis 交给线程池来管理,使用 Jedis 对象时,从连接池获取 Jedis,使用完成之后,再还给连接池。
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 连接池最大连接数
jedisPoolConfig.setMaxTotal(20);
// 只要是连接池的相关配置都可以通过JedisPoolConfig来进行修改
// redisHost和redisPort是实例的IP和端口
// redisPassword是实例的密码
// timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
JedisPool jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, timeout, redisPassword);
// 执行命令如下:
Jedis jedis = null;
try {jedis = jedisPool.getResource();// 具体的命令// jedis.set()
} catch (Exception e) {logger.error(e.getMessage(), e);
} finally {// 注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。if (jedis != null){jedis.close();}
}
Jedis连接池是基于apache-commons pool2实现的。在构建连接池对象的时候,需要提供池对象的配置对象,及JedisPoolConfig(继承自GenericObjectPoolConfig)。我们可以通过这个配置对象对连接池进行相关参数的配置(如最大连接数,最大空数等)。
使用Jedis连接池之后,在每次用完连接对象后一定要记得把连接归还给连接池。Jedis对close方法进行了改造,如果是连接池中的连接对象,调用Close方法将会是把连接对象返回到对象池,若不是则关闭连接。可以查看如下代码
Jedis也可以使用try-with-resources,try-with-resources是Java当中基于try-catch的一个语法糖。可以帮我们省略close的代码。
关于语法糖的本节不细讲了,感兴趣的看一下这篇文章:https://blog.csdn.net/weixin_43888891/article/details/124567498
try (Jedis jedis = pool.getResource()) {jedis.set("clientName", "Jedis");
}
对每个命令使用带有资源的try块可能会很麻烦,因此您可以考虑使用JedisPooled。
JedisPooled jedis = new JedisPooled("localhost", 6379);
现在你可以像绝地武士一样发送命令了。
jedis.sadd("planets", "Venus");
四、连接池参数
public class RedisPoolUtils {private static JedisPool jedisPool = null;/*** redis服务器地址*/private static String addr = "127.0.0.1";/*** redis服务器端口*/private static int port = 6379;/*** redis服务器密码*/private static String auth = "111111";static {try {JedisPoolConfig config = new JedisPoolConfig();// 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认trueconfig.setBlockWhenExhausted(true);// 设置的逐出策略类名, 默认DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数)config.setEvictionPolicyClassName("org.apache.commons.pool2.impl.DefaultEvictionPolicy");// 是否启用pool的jmx管理功能, 默认trueconfig.setJmxEnabled(true);// MBean ObjectName = new ObjectName("org.apache.commons.pool2:type=GenericObjectPool,name=" + "pool" + i); 默认为"pool", JMX不熟,具体不知道是干啥的...默认就好.config.setJmxNamePrefix("pool");// 是否启用后进先出, 默认trueconfig.setLifo(true);// 最大空闲连接数, 默认8个config.setMaxIdle(8);// 最大连接数, 默认8个config.setMaxTotal(8);// 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1config.setMaxWaitMillis(-1);// 逐出连接的最小空闲时间 默认1800000毫秒(30分钟)config.setMinEvictableIdleTimeMillis(1800000);// 最小空闲连接数, 默认0config.setMinIdle(0);// 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3config.setNumTestsPerEvictionRun(3);// 对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数 时直接逐出,不再根据MinEvictableIdleTimeMillis判断 (默认逐出策略)config.setSoftMinEvictableIdleTimeMillis(1800000);// 在获取连接的时候检查有效性, 默认falseconfig.setTestOnBorrow(false);// 在空闲时检查有效性, 默认falseconfig.setTestWhileIdle(false);// 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1config.setTimeBetweenEvictionRunsMillis(-1);jedisPool = new JedisPool(config, addr, port, 3000, auth);} catch (Exception e) {e.printStackTrace();}}/*** 获取 Jedis 资源** @return*/public static Jedis getJedis() {if (jedisPool != null) {return jedisPool.getResource();}return null;}/*** 释放Jedis资源*/public static void close(final Jedis jedis) {if (jedis != null) {jedis.close();}}
}
五、哨兵模式
哨兵模式简单的说,就是一台主机,一台备机,外加一台监控服务,当监控服务观测到主机已经宕机,就会将备用机切换成主机,以便继续提供服务。
public class RedisPoolUtils {private static Jedis jedis;private static JedisSentinelPool jedisSentinelPool;static{try {JedisPoolConfig config = new JedisPoolConfig();//最大空闲连接数, 默认8个config.setMaxIdle(8);//最大连接数, 默认8个config.setMaxTotal(8);//最小空闲连接数, 默认0config.setMinIdle(0);//获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1config.setMaxWaitMillis(3000);//在获取连接的时候检查有效性,表示取出的redis对象可用, 默认falseconfig.setTestOnBorrow(true);//redis服务器列表Set<String> sentinels = new HashSet<>();sentinels.add(new HostAndPort("192.168.43.212", 26379).toString());sentinels.add(new HostAndPort("192.168.43.213", 26379).toString());sentinels.add(new HostAndPort("192.168.43.214", 26379).toString());//初始化连接池jedisSentinelPool = new JedisSentinelPool("mymaster", sentinels, config, "111111");// 从池中获取一个Jedis对象jedis = jedisSentinelPool.getResource();} catch (Exception e) {e.printStackTrace();}}
}
六、集群模式
为了保证高可用,redis-cluster集群通常会引入主从复制模型,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。
public class RedisPoolUtils {static{try {JedisPoolConfig config = new JedisPoolConfig();//最大空闲连接数, 默认8个config.setMaxIdle(8);//最大连接数, 默认8个config.setMaxTotal(8);//最小空闲连接数, 默认0config.setMinIdle(0);//获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1config.setMaxWaitMillis(3000);//在获取连接的时候检查有效性,表示取出的redis对象可用, 默认falseconfig.setTestOnBorrow(true);Set<HostAndPort> nodes = new HashSet<>();nodes.add(new HostAndPort("192.168.43.212", 26379));nodes.add(new HostAndPort("192.168.43.213", 26379));nodes.add(new HostAndPort("192.168.43.214", 26379));JedisCluster jedisCluster = new JedisCluster(nodes, config);jedisCluster.set("key", "hello world");jedisCluster.close();} catch (Exception e) {e.printStackTrace();}}
}
七、Springboot当中使用Jedis
在springboot当中一般我们使用redis都是通过引入spring-boot-starter-data-redis进行使用的。该starter主要是帮我们屏蔽了Redis客户端之间的差异,给我们提供了统一访问方式RedisTemplate,他支持使用Jedis和lettuce,默认情况下是使用的lettuce。
一般情况下我们也不会使用Jedis,都是使用默认的lettuce
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
我们可以排除掉lettuce的依赖,然后引入jedis的依赖。
<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><!--springboot当中对jedis做了版本管理,所以我们不需要声明版本号-->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>
然后添加如下配置
spring:redis:host: 127.0.0.1port: 6379
# password:# 指定客户端为jedisclient-type: jedisjedis:pool:# 是否开启连接池enabled: true# maxactive和maxidle这两个值最好设置得比较接近一些,不然maxidle设置得太小,单方面把maxactive调得很高,这样会导致频繁的连接销毁和新建,这跟连接池的理念就相悖了。# 最主要参数,配置连接池同时能维持的最大连接数,如果客户端理论上需要100个连接,则这个值设为100。max-active: 8# 如果客户端一段时间内不需要使用连接,又一直把所有连接池中的所有连接都维持在活动状态是很浪费资源的,所以maxIdle这个选项告诉连接池,如果客户端没有需求,那么最多维持maxIdle个空闲连接。max-idle: 8# 和maxIdle类似,maxIdle告诉连接池最多维持多少个空闲连接,minIdle告诉tomcat即使客户端没有需求,也要至少维持多少个空闲连接,以应对客户端的突发需求。min-idle: 0# 连接池出借连接的最长期限,单位是毫秒max-wait: 5s# 空闲对象逐出器线程的运行间隔时间。当为正值时,空闲对象逐出器线程启动,否则不执行空闲对象逐出。time-between-eviction-runs: 1sconnect-timeout: 2000
配置RedisTemplate序列化
@Configuration
public class RedisConfiguration {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate();redisTemplate.setConnectionFactory(factory);Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);redisTemplate.setKeySerializer(stringRedisSerializer);redisTemplate.setHashKeySerializer(stringRedisSerializer);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
测试:可以选择使用RedisTemplate 来操作redis,也可以通过JedisConnectionFactory 来获取Jedis实例来操作redis。这里获取到的Jedis记住一定要用完还给连接池。
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.Jedis;@SpringBootTest
class JedisDemoApplicationTests {@Autowiredprivate JedisConnectionFactory jedisConnectionFactory;@Autowiredprivate RedisTemplate redisTemplate;@Testvoid contextLoads1() {// 可以读取连接池的配置GenericObjectPoolConfig<Jedis> poolConfig = jedisConnectionFactory.getPoolConfig();System.out.println(poolConfig.getMaxWaitDuration());Jedis jedis = getJedis();jedis.set("aaa", "1111");jedis.close();// 注意使用jedis实例向redis当中set值的时候是并没有序列化的,而redisTemplate取值是会进行反序列化的,所以假如不是json的话可能会异常!System.out.println(redisTemplate.opsForValue().get("aaa"));}/*** 从连接池当中获取jedis实例** @return*/private Jedis getJedis() {return (Jedis) jedisConnectionFactory.getConnection().getNativeConnection();}
}
八、Springboot源码分析
源码当中我们重点观察这三个类RedisAutoConfiguration、LettuceConnectionConfiguration、LettuceConnectionConfiguration。
其中RedisAutoConfiguration会给我们创建一个RedisTemplate放入到容器当中。然后他创建的时候需要依赖一个RedisConnectionFactory连接工厂。
默认它会加载Lettuce的连接工厂类LettuceConnectionFactory。
我们也可以通过排除Lettuce的依赖,然后引入Jedis的依赖
第三章我们说了连接池有很多参数,但是我们通过application只能配置一部分参数,假如我们想要配置更多的参数可以手动实例化JedisConnectionFactory来创建连接池,这样就可以使用JedisPoolConfig来修改连接池配置了。。
@Configuration
public class RedisConfig {private RedisConnectionFactory connectionFactory = null;@Bean(name = "redisConnectionFactory")public RedisConnectionFactory initConnectionFactory() {if (this.connectionFactory != null) {return this.connectionFactory;}JedisPoolConfig poolConfig = new JedisPoolConfig();// 最大空闲数poolConfig.setMaxIdle(50);// 最大连接数poolConfig.setMaxTotal(100);// 最大等待毫秒数poolConfig.setMaxWaitMillis(2000);// 创建Jedis连接工厂JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);// 配置Redis连接服务器RedisStandaloneConfiguration rsc = connectionFactory.getStandaloneConfiguration();//本机的IP地址,可以通过cmd窗口ipconfiig查看rsc.setHostName("10.160.64.151");rsc.setPort(6379);//redis安装后默认是没有密码的//rsc.setPassword(RedisPassword.of("cwc8469619"));this.connectionFactory = connectionFactory;return connectionFactory;}
}