一、客户端介绍
- Jedis
以Redis命令作为方法名称,学习成本低,简单实用。但是Jedis实例是线程不安全的,多线程环境下需要基于连接池来使用。
- lettuce
Lettuce是基于Netty实现的,支持同步、异步和响应式编程方式,并且是线程安全的。支持Redis的哨兵模式、集群模式和管道模式。
- Redisson
Redisson是一个基于Redis实现的分布式、可伸缩的Java数据结构集合。包含了诸如Map、Queue、Lock、Semaphore、AtomicLong等强大功能
二、Jedis
Jedis官方文档:https://github.com/redis/jedis
2.1 引入依赖
<!--jedis依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.0-m2</version>
</dependency>
2.2 建立连接
@BeforeEach
public void setUp() {
// 建立连接
jedis = new Jedis("101.32.333.188", 6379);
// 设置密码
jedis.auth("123456");
// 选择库
jedis.select(0);
}
2.3 测试使用
// 测试string
@Test
public void testString() {
// 插入数据,jedis命令和redis命令一致,直接使用即可
String result = jedis.set("name", "测试");
System.out.println("result = " + result);
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
// 测试hash
@Test
public void testHash() {
// 插入hash数据
jedis.hset("user:1", "name", "测试");
jedis.hset("user:1", "age", "25");
// 获取key单个数值
String name = jedis.hget("user:1", "name");
// 获取key所有数据
Map<String, String> hashMap = jedis.hgetAll("user:1");
System.out.println("name = " + name);
System.out.println("map = " + hashMap);
}
2.4 释放jedis
@AfterEach
public void release() {
// 释放资源
if (jedis != null) {
jedis.close();
}
}
三、Jedis连接池
Jedis本身是线程不安全的,并且频繁的创建喝销毁连接会有性能损耗,因此推荐使用Jedis连接池代替Jedis直连方式
3.1 建立配置类
public class JedisConnectFactory {
private static final JedisPool jedisPool;
static {
// 配置连接池
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大连接
jedisPoolConfig.setMaxTotal(8);
// 最大空闲连接
jedisPoolConfig.setMaxIdle(8);
// 最小空闲连接
jedisPoolConfig.setMinIdle(0);
// 设置最长等待时间,单位ms
jedisPoolConfig.setMaxWait(Duration.ofMillis(200));
// 连接
jedisPool = new JedisPool(jedisPoolConfig, "101.32.333.188", 6379, 1000, "123456");
}
// 获取jedis对象
public static Jedis getJedis() {
return jedisPool.getResource();
}
}
3.2 测试使用
@BeforeEach
public void setUp() {
// 建立连接,直接调用连接池方法获取Jedis实例
jedis = JedisConnectFactory.getJedis();
// 设置密码
jedis.auth("123456");
// 选择库
jedis.select(0);
}
四、SpringDataRedis
SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis
- 提供了对不同Redis客户端的整合(Lettuce和Jedis)
- 提供了RedisTemplate统一API来操作Redis
- 支持Redis的发布订阅模型
- 支持Redis哨兵和Redis集群
- 支持基于Lettuce的响应式编程
- 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
- 支持基于Redis的JDKCollection实现
4.1 相关API命令
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:
API | 返回值类型 | 说明 |
---|---|---|
redisTemplate.opsForValue | ValueOperations | 操作String类型数据 |
redisTemplate.opsForHash | HashOperations | 操作Hash类型数据 |
redisTemplate.opsForList | ListOperations | 操作List类型数据 |
redisTemplate.opsForSet | SetOperations | 操作Set类型数据 |
redisTemplate.opsForZSet | ZSetOperations | 操作SortedSet类型数据 |
redisTemplate | 通用的命令 |
4.2 引入依赖
SpringBoot已经提供了对SpringDataRedis的支持,直接引入依赖即可:
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--连接池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
4.3 配置文件
注:spring-data-redis 默认就引入了lettuce依赖,如果需要使用Jedis,则需额外添加jedis依赖
<!--jedis依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring:
redis:
host: 101.32.333.188
port: 6379
password: 123456
lettuce:
pool:
max-active: 8 # 最大连接
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
max-wait: 100 # 最长连接等待时间
4.4 注入RedisTemplate
@Resource
private RedisTemplate redisTemplate;
4.5 编写测试
@SpringBootTest
public class RedisDemoApplicationTests {
@Resource
private RedisTemplate redisTemplate;
@Test
void testString() {
// 插入一条String数据
redisTemplate.opsForValue().set("name", "实践");
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
}
4.6 SpringDataRedis的序列化方式
RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的:
存入的key变成了这样:"\xac\xed\x00\x05t\x00\x04name"
缺点:
- 可读性差
- 内存占用较大
序列化工具类:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 创建RedisTemplate对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 创建JSON的序列化工具
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置key的序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置value的序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 返回
return redisTemplate;
}
}
// 如果只是简单demo测试,没添加Jackson依赖会出现报错,因为使用Json的序列化工具,正常SpringMvc中是已经包含Jackson依赖,不需要额外添加
<!--Jackson依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
4.6.1 测试存入对象
- 创建对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private Integer age;
}
- 单元测试
@Test
void testSaveUser() {
// 写入数据
redisTemplate.opsForValue().set("user:23", new User("cy", 25));
// 获取数据
User user = (User) redisTemplate.opsForValue().get("user:23");
System.out.println("user = " + user);
}
- 输出结果
1、控制台输出
user = User(name=cy, age=25)
2、redis可视化工具查看,对象value已经被转成json格式存储了
{
"@class": "com.cy.common.pojo.User",
"name": "cy",
"age": 25
}
4.6.2 JSON序列化器弊端
{
"@class": "com.cy.common.pojo.User",
"name": "cy",
"age": 25
}
为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。
为了节省内存空间,我们并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。
4.6.3 StringRedisTemplate
Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了自定义RedisTemplate的过程:
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
void testStringTemplate() {
// 创建对象
User user = new User("cyy", 25);
// 手动序列化
String json = JSONObject.toJSONString(user);
// 写入数据
stringRedisTemplate.opsForValue().set("user:24", json);
// 读取数据
String value = stringRedisTemplate.opsForValue().get("user:24");
// 反序列化
User user1 = JSONObject.parseObject(value, User.class);
System.out.println("user =" + user1);
}
4.6.4 RedisTemplate的两种序列化方案:
方案一:
- 自定义RedisTemplate
- 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
方案二:
- 使用StringRedisTemplate
- 写入Redis时,手动把对象序列化为JSON
- 读取Redis时,手动把读取到的JSON反序列化为对象
五、Redisson
Redisson是一个Redis的基础上实现的Java驻内存数据网络(In-Memory Data Frid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qg4vtevY-1677636456404)(https://note.youdao.com/yws/api/personal/file/43CC4A08D9794A2D803A18CE9E4EDB02?method=download&shareKey=c3fe38bb66e51537b4940c2605b2f702)]
基于setnx实现的分布式锁存在下面的问题:
- 不可重入:同一个线程无法多次获取同一把锁
- 不可重试:获取锁只尝试一次就返回false,没有重试机制
- 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患。
- 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从未同步主中锁数据,则会出现锁失效
5.1 Redisson上手使用
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.3</version>
</dependency>
2.配置Redisson客户端:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置类
Config config = new Config();
// 添加redis地址,setAddress添加单节点地址,也可以使用 config.useClusterServers().setNodeAddresses() 添加集群地址
config.useSingleServer().setAddress("redis://111.22.268.127:6379").setPassword("123456");
// 创建客户端
return Redisson.create(config);
}
}
- 使用Redisson的分布式锁
// 引入Redisson客户端
@Resource
RedissonClient redissonClient;
// 创建锁
RLock lock = redisson.getLock("lockKey");
// 最常见的使用方法
lock.lock();
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
5.2 Redisson可重入锁原理
在原有判断是否是同一线程标识的基础上,增加可重入逻辑,同一线程执行业务获取锁,增加锁计数器+1,执行完毕锁计数器-1,直到锁计数器为0,则释放锁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1J2OyBUR-1677636456414)(https://note.youdao.com/yws/api/personal/file/151A609E1613476AB4B383CC9575CDD6?method=download&shareKey=db981c6d893000a385adc3dcd333f007)]
// RedissonLock 源码中的获取锁 lua脚本
if ((redis.call('exists', KEYS[1]) == 0) or
(redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
// RedissonLock 源码中的释放锁 lua脚本
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
5.3 Redisson的锁充实和WatchDog机制
获取锁流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SoplrnJw-1677636456415)(https://note.youdao.com/yws/api/personal/file/F4A4F4540E6C4F75B8B7A49C6C7892F2?method=download&shareKey=c94727231fff7307ca6d699e1f1bb0cc)]
释放锁流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I1x7NdcZ-1677636456416)(https://note.youdao.com/yws/api/personal/file/F5BD911795624469A97F1674DF191645?method=download&shareKey=ae1e4f5978e0f76545193c5f99f9a4e0)]
Redisson分布式锁原理:
- 可重入:利用hash结构记录线程id和重入次数
- 可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
- 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间
5.4 Redisson分布式锁主从一致性问题
5.4.1 不可重入的Redis分布式锁:
- 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标识
- 缺陷:不可重入、无法重试、锁超时失效
5.4.2 可重入的Redis分布式锁:
- 原理:利用hash结构,记录线程标识和冲入次数;利用watchDog机制延续锁时间;利用信号量控制锁重试等待
- 缺陷:redis宕机引起锁失效问题
5.4.3 Redisson的multiLock:
- 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才能算获取锁成功
- 缺陷:运维成本高、实现复杂