redis专题总结
1. Redis 支持哪些数据类型
- String
存储结构:key-value形式
使用场景:
* 缓冲-缓存数据,提高查询效率
* 计数器-incr、decr命令实现访问计数
* 分布式锁-setnx命令实现分布式锁
* Session共享-存储用户会话信息
- Hash
存储结构:field-value组成的map,适合存储对象
使用场景:
* 存储对象:用户信息、商品信息等
* 购物车:用户id作为key、field作为商品id、value作为数量
- List
存储结构:双向链表、有序、可重复集合
使用场景:
* 消息队列
* 最新列表:朋友圈动态、最新文章等
* 历史记录:用户搜索历史、浏览记录等
- Set
存储结构:哈希表、无序、不可重复集合
使用场景:
* 标签系统:用户标签、文章标签
* 共同好友:sinter求2个set集合的交集
- Sorted Set
存储结构:Set集合基础上为每个元素关联一个分数score、用于排序
使用场景:
* 排行榜:积分排行、热搜排行
2. Redis 的特性
- 基于内存存储,速度极快
数据主要在内存中。读写性能极高。
- 支持丰富的数据结构
支持String、Hash、List、Set、Sorted Set等数据类型。
- 单线程与原子性
命令操作是原子的,无需担心并发竞争。
- 持久化可选
支持RDB与AOF持久化。
- 发布订阅与Lua脚本
支持消息通信与复杂原子逻辑
3. Redis 为什么单线程操作的
前言:这里的单线程操作是指在redis中,同一个时间点只用一个CPU线程来处理客户端请求(即:同一个时刻只有一个命令在执行)
这样设计的原因:
1. 单线程避免了多线程的锁竞争。因为Redis处理速度非常快,不用太考虑锁等待的时间。
2. 单线程保证了每个redis命令的原子性,要么成功要么失败。
4. Redis 的持久化
持久化:因为redis是一个内存数据库,一旦redis重启后数据就会丢失。持久化就是将redis的数据持久化保存到硬盘上。
Redis 的持久化方案:
- RDB
- AOF
4.1 RDB持久化(默认开启)
会在一定时间内检测key的变化,达到要求就会将内存种的数据集快照写入磁盘,它恢复时是将快照文件直接读取到内存里。
RDB保存的文件是dump.rdb。
触发方式:
- 手动执行SAVE或者BGSAVE命令
- 配置自动触发(redis配置文件中配置)
save 900 1 # 表示的就是如果900秒内,至少有一个key进行了修改,就持久化数据。
- 执行SHUTDOWN、FLUSHALL命令时也会触发
4.2 AOF持久化
记录所有写入操作(如SET、INCR、LPUSH 等),逐条追加写入一个日志文件(默认文件名为 appendonly.aof,可在配置文件自己重新指定文件名)。
需要我们手动开启:
# 将配置文件中 appendonly 设置为yes
appendonly yes
aof的写入策略:
# 默认是每秒同步一次写入数据。
# 支持3种写入策略,可自行配置。 eversec(每秒同步) always(每个写入操作都同步) no(由操作系统决定何时同步,一般不会用这种)
appendfsync eversec
4.3 RDB与AOF生产环境下的抉择
建议:同时启用RDB与AOF。RBD用于定期备份和快速恢复数据,AOF保证数据安全性。
配置示例:
save 900 1 # 指定RBD自动触发条件
appendonly yes # 开启AOF
appendsync eversec # 指定AOF的写入策略
aof-use-rdb-preamble #开启混合持久化
5. Redis 过期删除策略
问题:redis种若有一个key只能存活1小时,那么redis是如何对这个key删除的?
redis有3种删除策略:
- 定期删除 每隔100ms检查16个数据库(reids默认自带有16个数据)随机抽取每个库中20个设置过期时间的key,判断其是否过期,是就删除
- 惰性删除 若定期删除没有随机抽取到那个过期的key,也不怕,惰性删除会在你查询那个key时,先去判断其是否过期,是就删除
- 定时删除 在设置键过期的同时,创建一个定时器,让定时器在键过期时间来临时,执行对键的删除操作。
Redis采用 定期+惰性 的删除策略
6. Redis 常用命令
# 设置key和value值,若当前key不存在时,若存在则不执行该操作
setnx key value
# 设置key对应的value,并设置过期时间
setex key seconds value
# 为指定key设置过期时间
expire key seconds
# 查询指定key剩余过期时间
ttl key
# 返回指定key的value类型
type key
# 为指定key修改key名称
rename oldkey newkey
# 手动触发快照持久优化
SAVE # 同步执行快照持久化,期间会阻塞其他命令的执行,直到完成。
BGSAVE # 异步执行快照持久,不会阻塞其他命令的执行。(生产环境推荐使用)
7. SpringBoot整合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>
- 配置文件
spring:
redis:
host: localhost
port: 6379
password: 123456 # 如果没有密码可不配置
database: 0 # 默认使用0号数据库
timeout: 3000ms # 连接超时时间
lettuce: # Lettuce 是一个高性能的 Redis Java 客户端,Spring Boot 2.x 默认使用 Lettuce 替代 Jedis。
pool:
max-active: 8 # 连接池最大连接数
max-idle: 8 # 连接池最大空闲连接数
min-idle: 0 # 连接池最小空闲连接数
max-wait: -1ms # 连接池最大阻塞等待时间
- 配置类(默认的RedisTemplate存储的是二进制数据,不可读,存储占用空间大。一般我们需要自定义RedisTemplate)
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 启用默认类型信息,解决反序列化类型问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
8. 分布式锁
锁的意义:防止多线程中共享资源的数据正确性。
单体项目中(只部署一台服务器时,我们完全可以使用同步代码块、同步方法、Lock锁这种方法去实现),但是一旦部署集群就不行了,例如:部署2台服务器,同一时刻2台服务器都收到请求去扣减库存,就有可能出现脏数据,因为前面说的那3种方式只保证了自己那台服务器的线程安全,它的锁只针对那台服务器。
实现分布式的方案:
- redis(最主流)
- mysql
- zookeeper
redis实现分布式锁的大概思路:
- 核心思想:利用redis是单线程的特点(哪怕同一时刻来了10000个redis请求命令,也只能一个个执行),redis有个命令
setnx key value(设置key和value值,若当前key不存在时,若存在则不执行该操作)
核心实现步骤:
- 将扣减库存的代码,放在redis(先利用
setnx key value设值,看设的进去不,设的进去,就表示拿到了锁)后。 - 成功扣减库存后,在执行删除redis(把
setnx key value设置的值删掉)。
@Autowired
private StringRedisTemplate stringRedisTemplate;
private Integer num =100;
@RequestMapping("/deductStock")
public String deductStock(){
Boolean bool=stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "msj");
if(!bool){
System.out.println("扣减库存失败");
return "end";
}
if (num > 0) {
num -= 1;
} else {
return "库存不足";
}
stringRedisTemplate.delete("1ockKey");
return "end";
}
核心实现步骤优化:
- 这套代码,还有很多问题需要处理(比较繁琐,有多种异常情况需要考虑)。
情况一:如果设置redis后,扣减库存代码出现异常,redis一直没有删除,导致死锁。可以用try catch finally实现,在finally中删除redis
情况二:如果解决了情况一,此时也设置了redis,服务器宕机了,finally代码块都还没来得及执行。可对redis设置过期时间,哪怕出现问题,到时间后,redis会删除,解决死锁
@Autowired
private StringRedisTemplate stringRedisTemplate;
private Integer num =100;
@RequestMapping("/deductStock")
public String deductStock(){
try {
Boolean bool=stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "msj", 10, TimeUnit.SECONDS);
if(!bool){
System.out.println("扣减库存失败");
return "end";
}
if (num > 0) {
num -= 1;
} else {
return "库存不足";
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
stringRedisTemplate.delete("1ockKey");
}
return "end";
}
情况三:如果解决了情况一二,我现在扣减库存的代码耗时长,我需要15秒,当我在执行到第10秒的时候(reids过期自动删除了),恰在此时又来了个请求,它成功用redis的setnx key value设置了值,他就开始执行它的扣减库存,它才执行了5秒,我第一个扣减库存这时才执行完了。它就去执行删除redis,导致把人家第二个请求设置的reids给删除了。设置redis时,将线程id作为值进行存储,删除redis时,需要判断是否为当前线程,是的话才删除,从而避免删除人家的reids
@Autowired
private StringRedisTemplate stringRedisTemplate;
private Integer num =100;
@RequestMapping("/deductStock")
public String deductStock(){
String threadId = String.valueOf(Thread.currentThread().getId());
try {
Boolean bool=stringRedisTemplate.opsForValue().setIfAbsent("lockKey", threadId, 10, TimeUnit.SECONDS);
if(!bool){
System.out.println("扣减库存失败");
return "end";
}
if (num > 0) {
num -= 1;
} else {
return "库存不足";
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (threadId.equals(stringRedisTemplate.opsForValue().get("lockKey"))) {
stringRedisTemplate.delete("1ockKey");
}
}
return "end";
}
情况四:虽然你解决了情况一二三,但是现在需要执行15秒的那个请求还在执行,而10秒时,redis已经过期了,就导致另一个请求能执行删除库存,这样导致有可能还是出现超卖现象。应该要能实现锁的续期,如果这个请求耗时15秒,10秒应该过期时,应该对其续期,从而避免其他请求进行扣减库存。解决思路就是搞一个定时任务,每隔一段时间判断当前线程是否执行完毕,没有则增加redis的过期时间,太繁琐了。有专门的分布式锁框架Redisson
9. Redisson分布式锁
- 添加依赖
<dependencies>
<!-- Spring Boot Redis Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.6</version>
</dependency>
</dependencies>
- 配置文件
# application.yml
spring:
redis:
host: localhost
port: 6379
password: yourpassword # 如果你的Redis设置了密码
# Redisson配置
redisson:
singleServerConfig:
address: "redis://${spring.redis.host}:${spring.redis.port}"
password: ${spring.redis.password} # 如果你的Redis设置了密码
# 其他Redisson配置...
- 使用
@Service
public class LockService {
@Autowired
private RedissonClient redissonClient;
public void lockExample() {
RLock lock = redissonClient.getLock("myLock");
lock.lock(); // 获取锁,阻塞等待直到获取锁为止。如果锁已经被占用,则等待。
try {
// 执行你的代码块...
} finally {
lock.unlock(); // 释放锁。即使在代码块执行过程中发生异常,也要确保释放锁。
}
}
}
浙公网安备 33010602011771号