文章目录
前言
Jedis和Lettuce
Jedis
和
Lettuce
的都是连接
Redis Server
的
客户端程序
。
Jedis
在实现上是
直连redis server,多线程环境下非线程安全,除非使用连接池,为每个Jedis实例增加物理连接
。
Lettuce
基于Netty
(事件驱动模型)的连接实例(StatefulRedisConnection),
可以在多个线程间并发访问,且线程安全
,满足多线程环境下的并发访问,
同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例
。
spring-data-redis
Spring Data Redis提供了从Spring应用程序轻松配置和访问Redis的功能。它提供了用于与存储交互的低级和高级抽象,使用户不必再关注基础设施
。
spring-data-redis
的优势:
可以方便地更换Redis的Java客户端,比Jedis多了自动管理连接池的特性,方便与其他Spring框架进行搭配使用如:SpringCache
。
spring-data-redis
提供了一个
RedisConnectionFactory
接口,通过它可以生成一个
RedisConnection
接口对象,而
RedisConnection
接口对象是
对Redis底层接口的封装
。例如,我们使用的
Jedis
驱动,那么Spring就会提供
RedisConnection
接口的实现类
JedisConnection
去封装原有的
Jedis(redis.clients.jedis.Jedis)
对象。
public interface RedisConnectionFactory extends PersistenceExceptionTranslator {
/**
* Provides a suitable connection for interacting with Redis.
*
* @return connection for interacting with Redis.
*/
RedisConnection getConnection();
/**
* Provides a suitable connection for interacting with Redis Cluster.
*
* @return
* @since 1.7
*/
RedisClusterConnection getClusterConnection();
/**
* Specifies if pipelined results should be converted to the expected data type. If false, results of
* {@link RedisConnection#closePipeline()} and {RedisConnection#exec()} will be of the type returned by the underlying
* driver This method is mostly for backwards compatibility with 1.0. It is generally always a good idea to allow
* results to be converted and deserialized. In fact, this is now the default behavior.
*
* @return Whether or not to convert pipeline and tx results
*/
boolean getConvertPipelineAndTxResults();
/**
* Provides a suitable connection for interacting with Redis Sentinel.
*
* @return connection for interacting with Redis Sentinel.
* @since 1.4
*/
RedisSentinelConnection getSentinelConnection();
}
在SpringDataRedis中是使用RedisConnection接口对象去操作Redis的。要获取RedisConnection接口对象,是通过RedisConnectionFactory接口去生成的,所以第一步要配置的便是这个工厂了
,而配置这个工厂主要是配置
Redis
的连接池,对于连接池可以限定其最大连接数、超时时间等属性。
@Configuration
public class RedisConfig {
private RedisConnectionFactory connectionFactory = null;
/**
* 若使用了spring-boot-autoconfigure,只需在application.yml中配置spring.redis和spring.redis.jedis/lettuce即可,JedisConnectionConfiguration/LettuceConnectionConfiguration会自动注册JedisConnectionFactory/LettuceConnectionFactory-Bean
*/
@Bean(name = "RedisConnectionFactory")
public RedisConnectionFactory initRedisConnectionFactory() {
if (this.connectionFactory != null) {
return this.connectionFactory;
}
JedisPoolConfig poolConfig = new JedisPoolConfig();
// 最大空闲数
poolConfig.setMaxIdle(30);
// 最大连接数
poolConfig.setMaxTotal(50);
// 最大等待毫秒数
poolConfig.setMaxWaitMillis(2000);
// 创建Jedis连接工厂
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
// 获取单机的Redis配置
RedisStandaloneConfiguration rsCfg = connectionFactory.getStandaloneConfiguration();
ConnectionFactory.setHostName("192.168.11.131");
ConnectionFactory.setPort(6379);
ConnectionFactory.setPassword("123456");
this.connectionFactory = connectionFactory;
return connectionFactory;
}
//......
}
但是我们在使用一条连接时,要先从
RedisConnectionFactory
工厂获取,然后在使用完成后还要自己关闭它。SpringDataRedis为了进一步简化开发,提供了
RedisTemplate
。
RedisTemplate是一个强大的类,首先它会自动从RedisConnectionFactory工厂中获取连接,然后执行对应的Redis命令,在最后还会关闭Redis的连接
。
spring-boot-starter-data-redis
spring-boot-starter-redis
在2017年6月后就改为了
spring-boot-starter-data-redis
,是基于
spring-data-redis
开发的Redis场景启动器。
阅读源码发现
spring-boot-starter-data-redis
没有任何代码,只是引入了
spring-data-redis
的依赖,其实Redis的自动配置代码都在
spring-boot-autoconfigure
中
关于
spring-boot-starter-data-redis
的源码分析参考
spring-boot-starter-data-redis源码解析与使用实战
SpringBoot2.x
后默认使用的不在是
Jedis
而是
lettuce
,所以
spring-boot-starter-data-redis
依赖了:
lettuce-core
、
spring-data-redis
、
spring-boot-starter
操作Redis数据
推荐使用
RedisTemplate
。
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<!--lettuce 依赖commons-pool-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
properties
# ====================================================================================
# spring-redis
# ====================================================================================
# 若没有密码,则需要注释这个配置项
# spring.redis.password=
spring.redis.timeout=10000
# 单机
# spring.redis.host=10.210.13.203
# spring.redis.port=6381
# 集群
spring.redis.cluster.nodes=10.210.13.203:6381,10.210.13.203:6382,10.210.13.203:6383,10.210.13.203:6384,10.210.13.203:6385,10.210.13.203:6386
spring.redis.cluster.maxRedirects=5
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-idle=8
完整配置项参考
org.springframework.boot.autoconfigure.data.redis.RedisProperties
config
package com.xx.yyy.config;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import java.time.Duration;
@Configuration
public class RedisConfiguration {
/**
* spring-boot-autoconfigure的RedisAutoConfiguration自动注册的RedisTemplate,使用的序列化器为默人的JdkSerializationRedisSerializer,序列化后生成的是不利于阅读的编码字符串。
* 所以我们手动注册一个RedisTemplate,设置RedisConnectionFactory属性为spring-boot-autoconfigure的JedisConnectionConfiguration/LettuceConnectionConfiguration自动注册的RedisConnectionFactory,并设置序列化器为StringRedisSerializer。
* 其实也可以直接在主启动类中使用@Autowired注入SpringBoot自动注册的RedisTemplate,并添加一个@PostConstruct的方法来修改它的序列化器为StringRedisSerializer。
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
final RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置ConnectionFactory,SpringBoot会自动注册ConnectionFactory-Bean
template.setConnectionFactory(redisConnectionFactory);
// 设置序列化器为StringRedisSerializer(默认是 JdkSerializationRedisSerializer , java操作时会产生乱码)
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(stringRedisSerializer);
template.setValueSerializer(new JdkSerializationRedisSerializer());
template.afterPropertiesSet();
return template;
}
/**
* 配置StringRedisTemplate
* @param factory
* @return
*/
@Bean
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory factory) {
StringRedisTemplate redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
/**
* 支持使用Spring缓存注解操作Redis。(注意:主启动类上必须加@EnableCaching,开启支持缓存注解!)
* 若使用了spring-boot-autoconfigure,只需在application.yml中配置spring.cacheh和spring.cache.redis即可,RedisCacheConfiguration会自动注册RedisCacheManager
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 初始化一个RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
// 默认不过期
defaultCacheConfig = defaultCacheConfig.entryTtl(Duration.ZERO)
// 设置 key为string序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
// 设置value为json序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new Jackson2JsonRedisSerializer<>(
Object.class)));
// 不缓存空值
// .disableCachingNullValues();
// 初始化RedisCacheManager
return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
}
}
Redis分布式锁
方式一(使用JedisCluster)
pom
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath />
</parent>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- 1.5的版本默认采用的连接池技术是jedis,2.0以上版本默认连接池是lettuce, 因为此次是采用jedis,所以需要排除lettuce的jar -->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- jedis客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2,使用jedis必须依赖它-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<optional>true</optional>
</dependency>
properties
spring:
redis:
password: xxxx123456
cluster:
nodes:
- 10.163.xx.yyy:7001
- 10.163.xxx.yyy:7002
- 10.163.xx.yyy:7003
- 10.163.xx.zzz:7001
- 10.163.xx.zzz:7002
- 10.163.xx.zzz:7003
jedis:
pool:
#最大连接数
max-active: 8
#最大阻塞等待时间(负数表示没限制)
max-wait: -1
#最小空闲
min-idle: 0
#最大空闲
max-idle: 8
#连接超时时间
timeout: 10000
config
package org.springframework.boot.autoconfigure.data.redis;// 这个类是SpringBoot-autoconfigure自带的
import java.time.Duration;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for Redis.
*
* @author Dave Syer
* @author Christoph Strobl
* @author Eddú Meléndez
* @author Marco Aust
* @author Mark Paluch
* @author Stephane Nicoll
*/
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {}
package com.xx.proxy.config;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
/**
* JedisCluster 配置
*
*/
@Configuration
public class JedisClusterConfig {
@Autowired
private RedisProperties redisProperties;
// 连接redis超时时间
private static final Integer CONNECTION_TIMEOUT = 5000;
// 等待redis响应超时时间
private static final Integer SO_TIMEOUT = 10000;
// 操作redis的重试次数
private static final Integer MAX_ATTEMPTS = 3;
private static final Integer MAX_POOL_SIZE = 100;
@Bean
public JedisCluster jedisCluster(){
// 添加集群的服务节点Set集合
Set<HostAndPort> hostAndPortsSet = new HashSet<>();
// 添加节点
List<String> nodes = redisProperties.getCluster().getNodes();
for(String node : nodes){
String[] host = node.split(":");
hostAndPortsSet.add(new HostAndPort(host[0], Integer.parseInt(host[1])));
}
// Jedis连接池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空闲连接数, 默认8个
jedisPoolConfig.setMaxIdle(redisProperties.getJedis().getPool().getMaxIdle());
// 最大连接数, 默认8个
jedisPoolConfig.setMaxTotal(MAX_POOL_SIZE);
//最小空闲连接数, 默认0
jedisPoolConfig.setMinIdle(redisProperties.getJedis().getPool().getMinIdle());
// 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞无时间限制, 默认-1
jedisPoolConfig.setMaxWaitMillis(redisProperties.getJedis().getPool().getMaxWait().toMillis());
//对拿到的connection进行validateObject校验
jedisPoolConfig.setTestOnBorrow(true);
JedisCluster jedis = new JedisCluster(
hostAndPortsSet, CONNECTION_TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, redisProperties.getPassword(), jedisPoolConfig);
return jedis;
}
}
package com.xx.proxy.utils;
import java.util.Collections;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.JedisCluster;
/**
* JedisCluster实现的分布式锁
*
*/
@Slf4j
public class RedisDistributeLockUtil {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final String UNLOCK_SUCCESS = "1";
// 分布式锁的默认过期时间:5秒
private static final Integer DEFAULT_LOCK_TIME = 2000;
/**
*
* @param jedis jedis客户端
* @param lockKey 分布式锁的key
* @param cilentId 尝试获取分布式锁的客户端(线程)
* @param expireTime 锁的过期时间,防止死锁
* @return 获取锁成功返回true,获取失败返回false
*/
public static boolean tryLock(JedisCluster jedis, String lockKey, String clientId, int expireTime) {
if(expireTime <= 0){
expireTime = DEFAULT_LOCK_TIME;
}
try {
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
} catch (Exception e) {
log.error("线程 "+clientId+" 获取分布式锁错误:", e);
return false;
}
return false;
}
/**
*
* @param jedis jedis客户端
* @param lockKey 分布式锁的key
* @param cilentId 尝试释放分布式锁的客户端(线程)
* @param script Lua脚本,以原子方式在redis中执行
* @return 释放锁成功返回true,释放失败返回false
*/
public static boolean unLock(JedisCluster jedis, String lockKey, String clientId, String script) {
try {
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(clientId));
if (UNLOCK_SUCCESS.equals(result.toString())) {
return true;
}
} catch (Exception e) {
log.error("线程 "+clientId+" 释放分布式锁错误:", e);
return false;
}
return false;
}
}
方式二(使用Redisson)