一、Redis讲解
1、redis入门
特点:基于 key-value 存储。支持多种数据结构:string(字符串)、list(列表)、set(集合)、zset(sorted set 有序集合)、hash(哈希)。通过内存存储、操作数据,支持持久化将数据存储在硬盘中。支持过期时间、事务。
1.1 数据类型
五种基础类型:string(字符串)、list(列表)、set(集合)、zset(sorted set 有序集合)、hash(哈希)
三种特殊类型:HyperLogLogs(基数统计)、Bitmap (位存储)、geospatial (地理位置)
作用:
- HyperLogLogs 举个例子,A = {1, 2, 3, 4, 5}, B = {3, 5, 6, 7, 9};那么基数(不重复的元素)= 1, 2, 4, 6, 7, 9;这个结构可以非常省内存的去统计各种计数
- Bitmap 即位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态。
- geospatial 可推算地理位置的信息: 两地之间的距离, 方圆几里的人
2.1 持久化:RDB和AOF机制详解
RDB 持久化
RDB 就是 Redis DataBase 的缩写,中文名为快照/内存快照,RDB持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值
触发方式
触发rdb持久化的方式有2种,分别是手动触发和自动触发。
手动触发
手动触发分别对应save和bgsave命令
-
save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用
-
bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短
具体流程如下:
- redis客户端执行bgsave命令或者自动触发bgsave命令;
- 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
- 如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作;
- 子进程先将数据写入到临时的rdb文件中,待快照数据写入完成后再原子替换旧的rdb文件;
- 同时发送信号给主进程,通知主进程rdb持久化完成,主进程更新相关的统计信息(info Persitence下的rdb_*相关选项)。
自动触发
在以下4种情况时会自动触发
-
redis.conf中配置
save m n
,即在m秒内有n次修改时,自动触发bgsave生成rdb文件; -
主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;
-
执行debug reload命令重新加载redis时也会触发bgsave操作;
-
默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作;
redis.conf中配置RDB
- Redis中默认的周期新设置
# 周期性执行条件的设置格式为
save <seconds> <changes>
# 默认的设置为:
save 900 1
save 300 10
save 60 10000
# 以下设置方式为关闭RDB快照功能
save ""
-
如果900秒内有1条Key信息发生变化,则进行快照;
-
如果300秒内有10条Key信息发生变化,则进行快照;
-
如果60秒内有10000条Key信息发生变化,则进行快照。读者可以按照这个规则,根据自己的实际请求压力进行设置调整
-
其它相关配置
# 文件名称
dbfilename dump.rdb
# 文件保存路径
dir /home/work/app/redis/data/
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
RDB优缺点
-
优点
- RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;
- Redis加载RDB文件恢复数据要远远快于AOF方式;
-
缺点
- RDB方式实时性不够,无法做到秒级的持久化;
- 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;
- RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;
- 版本兼容RDB文件问题;
针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决
AOF持久化
Redis是“写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。PS: 大多数的数据库采用的是写前日志(WAL),例如MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性。
如何实现AOF
AOF日志记录Redis的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)
redis.conf中配置AOF
默认情况下,Redis是没有开启AOF的,可以通过配置redis.conf文件来开启AOF持久化,关于AOF的配置如下:
# appendonly参数开启AOF持久化
appendonly no
# AOF持久化的文件名,默认是appendonly.aof
appendfilename "appendonly.aof"
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的
dir ./
# 同步策略
# appendfsync always
appendfsync everysec
# appendfsync no
# aof重写期间是否同步
no-appendfsync-on-rewrite no
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 加载aof出错如何处理
aof-load-truncated yes
# 文件重写策略
aof-rewrite-incremental-fsync yes
从持久化中恢复数据
其实想要从这些文件中恢复数据,只需要重新启动Redis即可
二、springboot集成Redis
目录
3、新增一个 Redis 配置类 RedisConfig.java,用于自定义配置redis,由于官网给出的redisTemplate不是很好用
5、使用切面RedisAspect来处理redis的异常,保证redis在异常的时候,系统依然可以工作
1、yml文件配置redis
【yml:】
spring:
# Redis 配置
redis:
# Redis 服务器地址
host: 121.26.184.41
# 连接端口号
port: 6379
# 数据库索引(0 - 15)
database: 0
# 连接超时时间(毫秒)
timeout: 10000
# lettuce 参数
lettuce:
pool:
# 最大连接数(使用负值表示没有限制) 默认为 8
max-active: 10
# 最大阻塞等待时间(使用负值表示没有限制) 默认为 -1 ms
max-wait: -1
# 最大空闲连接 默认为 8
max-idle: 5
# 最小空闲连接 默认为 0
min-idle: 0
【properties:】
# Redis 服务器地址
spring.redis.host=121.26.184.41
# 连接端口号
spring.redis.port=6379
# 数据库(0 - 15)
spring.redis.database= 0
# 超时时间(毫秒)
spring.redis.timeout=600000
# lettuce 参数
# 最大连接数(使用负值表示没有限制) 默认为 8
spring.redis.lettuce.pool.max-active=20
# 最大阻塞等待时间(使用负值表示没有限制) 默认为 -1
spring.redis.lettuce.pool.max-wait=-1
# 最大空闲连接 默认为 8
spring.redis.lettuce.pool.max-idle=5
# 最小空闲连接 默认为 0
spring.redis.lettuce.pool.min-idle=0
2、添加依赖信息
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
3、新增一个 Redis 配置类 RedisConfig.java,用于自定义配置redis,由于官网给出的redisTemplate不是很好用
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//Json序列化器
Jackson2JsonRedisSerializer 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.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(om);
//String序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用String序列化器
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
//value采用Json序列化器
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4、写一个RedisUtil工具类,方便后面直接调用
/**
* @author yu_xuanyuan
* @date 2022/9/23 10:16
*/
@Component
public class RedisUtil {
@Autowired
RedisTemplate<String,Object> redisTemplate;
/**
* setex
* @param key key
* @param value value
* @param time 过期时间
*/
public void setex(String key,Object value,long time){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
/**
* set
* String类型的set,无过期时间
* @param key key
* @param value value
*/
public void set(String key, Object value){
redisTemplate.opsForValue().set(key,value);
}
/**
* 批量设置key和value
* @param map key和value的集合
*/
public void mset(Map<String,Object> map){
redisTemplate.opsForValue().multiSet(map);
}
/**
* 如果key不存在,则设置
* @param key key
* @param value value
* @return 返回是否成功
*/
public Boolean setnx(String key,Object value){
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
/**
* 批量插入key,如果key不存在的话
* @param map key和value的集合
* @return 是否成功
*/
public Boolean msetnx(Map<String,Object> map){
return redisTemplate.opsForValue().multiSetIfAbsent(map);
}
/**
* String类型的get
* @param key key
* @return 返回value对应的对象
*/
public Object get(String key){
return redisTemplate.opsForValue().get(key);
}
/**
* 删除对应key
* @param key key
* @return 返回是否删除成功
*/
public Boolean del(String key){
return redisTemplate.delete(key);
}
/**
* 批量删除key
* @param keys key的集合
* @return 返回删除成功的个数
*/
public Long del(List<String> keys){
return redisTemplate.delete(keys);
}
/**
* 给某个key设置过期时间
* @param key key
* @param time 过期时间
* @return 返回是否设置成功
*/
public Boolean expire(String key, long time){
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
/**
* 返回某个key的过期时间
* @param key key
* @return 返回key剩余的过期时间
*/
public Long ttl(String key){
return redisTemplate.getExpire(key);
}
/**
* 返回是否存在该key
* @param key key
* @return 是否存在该key
*/
public Boolean exists(String key){
return redisTemplate.hasKey(key);
}
/**
* 给key的值加上delta值
* @param key key
* @param delta 参数
* @return 返回key+delta的值
*/
public Long incrby(String key, long delta){
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 给key的值减去delta
* @param key key
* @param delta 参数
* @return 返回key - delta的值
*/
public Long decrby(String key, long delta){
return redisTemplate.opsForValue().decrement(key, delta);
}
//hash类型
/**
* set hash类型
* @param key key
* @param hashKey hashKey
* @param value value
*/
public void hset(String key,String hashKey, Object value){
redisTemplate.opsForHash().put(key, hashKey, value);
}
/**
* set hash类型,并设置过期时间
* @param key key
* @param hashKey hashKey
* @param value value
* @param time 过期时间
* @return 返回是否成功
*/
public Boolean hset(String key, String hashKey,Object value, long time){
hset(key, hashKey, value);
return expire(key, time);
}
/**
* 批量设置hash
* @param key key
* @param map hashKey和value的集合
* @param time 过期时间
* @return 是否成功
*/
public Boolean hmset(String key, Map<String,Object> map, long time){
redisTemplate.opsForHash().putAll(key, map);
return expire(key, time);
}
/**
* 获取hash类型的值
* @param key key
* @param hashKey hashKey
* @return 返回对应的value
*/
public Object hget(String key, String hashKey){
return redisTemplate.opsForHash().get(key, hashKey);
}
/**
* 获取key下所有的hash值以及hashKey
* @param key key
* @return 返回数据
*/
public Map<Object,Object> hgetall(String key){
return redisTemplate.opsForHash().entries(key);
}
/**
* 批量删除
* @param key key
* @param hashKey hashKey数组集合
*/
public void hdel(String key, Object... hashKey){
redisTemplate.opsForHash().delete(key, hashKey);
}
/**
* 判断是否存在hashKey
* @param key key
* @param hashKey hashKey
* @return 是否存在
*/
public Boolean hexists(String key, String hashKey){
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
}
5、使用切面RedisAspect来处理redis的异常,保证redis在异常的时候,系统依然可以工作
/**
* @author yu_xuanyuan
* @date 2022/9/23 10:28
*/
@Aspect
@Slf4j
public class RedisAspect {
@Pointcut("execution(* com.study.springboot.common.utils.*(..))")
public void pointcut(){
}
@Around("pointcut()")
public Object handleException(ProceedingJoinPoint joinPoint){
Object result = null;
try {
result= joinPoint.proceed();
} catch (Throwable throwable) {
log.error("redis may be some wrong");
}
return result;
}
}