1. 简介
Redis前面的基础部分此处不做记录 , 本篇记录狂神说讲解的Redis配置及高级应用的知识.
Redis中文网
2. 特殊数据类型
2.1 geospatial 地理位置
朋友的定位, 附近的人, 打车距离计算?
Redis3.2版本新增了Geo. 这个功能可以推算地理位置的信息, 两地之间的距离, 方圆多少距离内的人等.
命令 | 描述 |
---|---|
Redis GEOHASH 命令 | 返回一个或多个位置元素的 Geohash 表示 |
Redis GEOPOS 命令 | 从key里返回所有给定位置元素的位置(经度和纬度) |
Redis GEODIST 命令 | 返回两个给定位置之间的距离 |
Redis GEORADIUS 命令 | 以给定的经纬度为中心, 找出某一半径内的元素 |
Redis GEOADD 命令 | 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中 |
Redis GEORADIUSBYMEMBER 命令 | 找出位于指定范围内的元素,中心点是由给定的位置元素决定 |
geoadd 添加地理位置
规则: 两级无法直接添加, 我们一般会下载城市数据, 直接通过java程序一次性导入
有效的经度从-180度到180度。
有效的纬度从-85.05112878度到85.05112878度。
当坐标位置超出上述指定范围时,该命令将会返回一个错误。城市经纬度查询网站
# 参数 key 值(经度,维度,名称)
127.0.0.1:6379> geoadd china:city 166.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing
(integer) 1
127.0.0.1:6379> geoadd china:city 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:city 120.15 30.28 hangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 125.14 42.9 xian
(integer) 1
geopos 获取当前定位, 一定是一个坐标值.
127.0.0.1:6379> geopos china:city beijing
1) 1) "166.40000134706497192"
2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city beijing shanghai
1) 1) "166.40000134706497192"
2) "39.90000009167092543"
2) 1) "121.47000163793563843"
2) "31.22999903975783553"
返回两个给定位置之间的距离。如果两个位置之间的其中一个不存在, 那么命令返回空值。
如果用户没有显式地指定单位参数, 那么
GEODIST
默认使用米作为单位。指定单位的参数 unit 必须是以下单位的其中一个:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
127.0.0.1:6379> GEODIST china:city beijing shanghai # 北京到上海的距离, 单位默认m
"4132638.7228"
127.0.0.1:6379> GEODIST china:city beijing shanghai km # 指定单位km
"4132.6387"
我附近的人? (获取附近人的地址, 定位), 通过半径来查询.
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
范围可以使用以下其中一个单位:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
在给定以下可选项时, 命令会返回额外的信息:
WITHDIST
: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。WITHCOORD
: 将位置元素的经度和维度也一并返回。WITHHASH
: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。COUNT
: 在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 使用 count 选项去获取前 N 个匹配元素.
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km # 经度110,维度30为中心, 距离该点1000km以内的城市
1) "chongqing"
2) "shenzhen"
3) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km # 经度110,维度30为中心, 距离该点500km以内的城市
1) "chongqing"
127.0.0.1:6379> GEORADIUS china:city 110 30 800 km withdist # 经度110,维度30为中心, 距离该点1000km以内的城市及距离
1) 1) "chongqing"
2) "341.9374"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist count 2 # 经度110,维度30为中心, 距离该点1000km以内的城市, 限定数量返回
1) 1) "chongqing"
2) "341.9374"
2) 1) "shenzhen"
2) "924.6408"
127.0.0.1:6379> GEORADIUS china:city 110 30 800 km withdist withcoord # 经度110,维度30为中心, 距离该点1000km以内的城市及位置信息
1) 1) "chongqing"
2) "341.9374"
3) 1) "106.49999767541885376"
2) "29.52999957900659211"
127.0.0.1:6379> GEORADIUS china:city 110 30 800 km withdist withhash
1) 1) "chongqing"
2) "341.9374"
3) (integer) 4026042091628984
georadiusbymember 这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是
GEORADIUSBYMEMBER
的中心点是由给定的位置元素
决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点.
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km # 找出指定位置元素(北京)为中心,周围1000km内的城市
1) "beijing"
geohash 返回一个或多个位置元素的 Geohash 表示。
127.0.0.1:6379> GEOHASH china:city beijing chongqing # 将二维的经纬度转换为一维的字符串,如果两个字符串越接近, 那么距离越近.
1) "xxn6fx8f350"
2) "wm5xzrybty0"
GEO底层的实现原理其实就是
Zset
, 可以使用Zset的命令操作数据.
127.0.0.1:6379> ZRANGE china:city 0 -1 # 获取所有元素
1) "chongqing"
2) "shenzhen"
3) "hangzhou"
4) "shanghai"
5) "beijing"
6) "xian"
127.0.0.1:6379> zrem china:city beijing # 移除指定元素
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqing"
2) "shenzhen"
3) "hangzhou"
4) "shanghai"
5) "xian"
2.2 Hyperloglog
Redis 2.8.9版本新增了Hyperloglog数据结构, Hyperloglog用于基数的统计.
优点: 占用的内存是固定的, 2^64个不同的元素, 只需要占用12KB内存. 如果要从内存角度来比较的话, Hyperloglog首先.
网页的UV统计(一个人访问一个网站多次, 还是算是一个人).
传统的方式, set集合保存用户的id, 然后就可以统计set集合中的元素数量作为标准判断. 这个方式如果保存大量的用户id, 就会比较麻烦, 我们的目的是为了计数, 而不是保存用户ID.
使用Hyperloglog统计UV任务, 只有0.81%误差, 可以忽略不计.
命令 | 描述 |
---|---|
Redis Pfmerge 命令 | 将多个 HyperLogLog 合并为一个 HyperLogLog |
Redis Pfadd 命令 | 添加指定元素到 HyperLogLog 中。 |
Redis Pfcount 命令 | 返回给定 HyperLogLog 的基数估算值。 |
127.0.0.1:6379> PFADD mykey a b c d e f g h i j # 创建第一组元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey # 统计mykey元素的基数数量
(integer) 10
127.0.0.1:6379> PFADD mykey2 i j z x c v b n m # 创建第二组元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2 # 统计mykey2元素的基数数量
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 # 合并两组元素到mykey3
OK
127.0.0.1:6379> PFCOUNT mykey3 # 查看并集的数量, 统计mykey3元素的基数数量
(integer) 15
2.3 Bitmaps
进行位存储.
统计用户信息, 活跃, 不活跃! 用户登录, 未登录! 365天打卡场景. >> 只有两个状态的都可以使用Bitmaps
.
Bitmaps 位图数据结构, 都是操作二进制位进行记录, 只有0和1两个状态.
使用bitmaps来记录一周的打卡.
127.0.0.1:6379> SETBIT sign 0 1 # 周一打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0 # 周二未打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0 # 周三未打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1 # 周四打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 4 1 # 周五打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 5 0 # 周六未打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 6 0 # 周日未打卡
查看周四, 周日是否打卡.
127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 6
(integer) 0
统计操作, 统计打卡的天数.
127.0.0.1:6379> BITCOUNT sign # 统计打开的天数, 只有三天打卡了
(integer) 3
3. Redis事务
Redis事务的本质, 就是一组命令的集合. 一个事务中的所有命令都会被序列化, 在事务执行过程中, 会按照顺序执行! ( 一次性, 顺序性, 排他性)
---- 队列 set.. set.. set... 执行----
==Redis事务事务没有隔离级别的概念.==
所有的命令在事务中, 并没有被执行, 只有发起执行命令的时候才会执行 ! (EXEC)
==Redis单条命令是保证原子性的, 但是事务是不保证原子性的.==
事务操作命令:
命令 | 描述 |
---|---|
Redis Exec 命令 | 执行所有事务块内的命令。 |
Redis Watch 命令 | 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。 |
Redis Discard 命令 | 取消事务,放弃执行事务块内的所有命令。 |
Redis Unwatch 命令 | 取消 WATCH 命令对所有 key 的监视。 |
Redis Multi 命令 | 标记一个事务块的开始。 |
开启事务并执行成功
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> EXEC # 执行事务(提交)
1) OK
2) OK
3) "v2"
4) OK
127.0.0.1:6379>
取消事务
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> DISCARD # 取消事务, 队列中的命令都不会执行
OK
队列中存在编译型异常命令(命令错误
), 事务中所有的命令都不会被执行.
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务失败, 所有的命令都不会被执行
(error) EXECABORT Transaction discarded because of previous errors.
队列中存在运行时异常命令( 例如java中的1/0 ), 如果事务队列中存在语法错误, 错误的不会被执行, 其他都会正常执行成功.
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> INCR k1 # 非数字字符串不能加1, 运行时异常
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> EXEC # 提交事务, 运行时异常的命令执行失败, 队列中的其他命令都正常执行
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) OK
5) "v3"
watch监控(乐观锁)
在学习watch监控之前, 我们先来了解一下悲观锁/乐观锁
相关知识.
悲观锁
顾名思义,每次去拿数据的时候都被认为别人会修改
,所以每次在拿数据的时候都会被锁上,这样别人想拿这个数据就会block直到它拿到锁,传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等,都是在做操作之前先锁上。
乐观锁
每次去拿数据的时候都认为别人不会修改
,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号, CAS等机制。
CAS机制
CAS(Compare And Swap), 比较并替换. CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新, 直到成功。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为新值B。
CAS的缺点
(1). CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
(2). 不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
扣减余额操作, 无加塞篡改,先监控再开启multi,保证两笔金额变动在同一个事务内.
正常执行成功
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> WATCH money # 监视money对象
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> INCRBY out 20
QUEUED
127.0.0.1:6379(TX)> exec # 事务正常结束, 期间数据没有发生变动, 事务可以正常执行成功
1) (integer) 80
2) (integer) 20
多线程修改值, 使用watch可以当作redis的乐观锁操作.
# 线程1
127.0.0.1:6379> WATCH money # 监控money
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> INCRBY money 20
QUEUED
127.0.0.1:6379(TX)> EXEC # 提交事务之前, 其他线程修改了被监控的money的值,导致事务提交失败.
(nil)
# 线程2
127.0.0.1:6379> GET money
"100"
127.0.0.1:6379> SET money 500
OK
使用unwatch
取消监控, 相当于释放锁.
127.0.0.1:6379> WATCH money # 监控money
OK
127.0.0.1:6379> DECRBY money 10
(integer) 90
127.0.0.1:6379> INCRBY out 10
(integer) 30
127.0.0.1:6379> UNWATCH # money修改后,取消监控
OK
127.0.0.1:6379> WATCH money # 重新监控money
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> EXEC # 提交事务,执行成功
1) (integer) 80
2) (integer) 40
4. SpringBoot整合Jedis
Jedis: 采用的直连, 多个线程操作的话, 是不安全的, 一般使用Jedis pool连接池, 更像BIO模式.
lettuce: 采用netty, 实例可以在多个线程中进行共享, 不存在线程不安全的情况, 可以减少线程数据, 更像NIO模式.
整合Jedis配置
spring:
redis:
host: 192.168.65.129
password: 123456
port: 6379
jedis:
pool:
max-idle: 50 # 连接池中最大空闲数
max-active: 100 # 连接池中最大连接数
min-idle: 10 # 连接池中最小空闲数
max-wait: 10000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
timeout: 2000 # 连接超时时间
整合Lettuce配置
spring:
redis:
# host: 192.168.65.129 # 单机
# port: 6379
password: 123456
timeout: 2000 # 连接超时时间
lettuce:
pool:
max-idle: 50 # 连接池中最大空闲数
max-active: 100 # 连接池中最大连接数
min-idle: 10 # 连接池中最小空闲数
max-wait: 10000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
shutdown-timeout: 100 # 关闭超时时间
cluster: # 集群配置
nodes:
- 192.168.65.129:7000
- 192.168.65.129:7001
- 192.168.65.129:7002
- 192.168.65.129:7003
- 192.168.65.129:7004
- 192.168.65.129:7005
max-redirects: 3
4.1 源码分析
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate") // 不存在才生效, 意味着可以自定义redisTemplate来替换默认的
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
// 默认的RedisTemplate, 没有过多的设置, Redis对象都是需要序列化!
// 两个泛型都是Object, Object类型需要我们自行强制转换
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean // 由于string是redis中最常用的类型,所以说单独提取出来默认的
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
4.2 自定义RedisTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {
// 为了开发方便, 一般使用<String,Object>
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// 序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // enableDefaultTyping已过期
om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// string序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 在使用注解@Bean返回RedisTemplate时, 同时配置hashKey和hashValue的序列化方式
// key采用String的序列化方式
redisTemplate.setKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
// 使用increment方法,需要使用StringRedisSerializer或GenericToStringSerializer序列化器
redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Object.class));
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// hash的value采用jackson的序列化方式
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
4.3 封装Redis工具类
package com.crys.bootluttuce.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 描述:Redis工具类
* @author crysw
* @date 2022/4/11 20:54
* @version 1.0
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key获取过期时间
* @param key 键, 不能为null
* @return 时间(秒), 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在, false 不存在
*/
public boolean hashKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值或多个
*/
public void del(String... key) {
if ((key != null && key.length > 0)) {
if ((key.length == 1)) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ================String===============
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存存入并设置时间
* @param key
* @param value
* @param time
* @return
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param increment 要增加的值(大于0)
* @return
*/
public long incr(String key, long increment) {
if ((increment < 0)) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, increment);
}
/**
* 递减
* @param key 键
* @param decrement 要减少的值(大于0)
* @return
*/
public long decr(String key, long decrement) {
if (decrement < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -decrement);
}
// ================Map===============
/**
* HashGet
* @param key 键, 不能为null
* @param item 项, 不能为null
* @return
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashkey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据, 如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据, 如果不存在将创建, 并设置过期时间
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意: 如果已存在的hash表有时间, 这里将会替换原有的时间
* @return true成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键, 不能为null
* @param item 项, 可以是多个, 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键, 不能为null
* @param item 项, 不能为null
* @return true存在, false不存在
*/
public boolean hHashkey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增, 如果不存在, 就会创建一个,并把新增后的值返回
* @param key 键
* @param item 项
* @param increment 要增加的值(大于0)
* @return
*/
public double hincr(String key, String item, double increment) {
return redisTemplate.opsForHash().increment(key, item, increment);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param decrement 要减少的值
* @return
*/
public double hdecr(String key, String item, double decrement) {
return redisTemplate.opsForHash().increment(key, item, -decrement);
}
// ================set===============
/**
* 根据key获取set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询, 是否存在
* @param key 键
* @param value 值
* @return true存在, false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值, 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值, 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的数据
* @param key 键
* @param values 值, 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ================list===============
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1 代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引获取list中的值
* @param key 键
* @param index 索引, index>=0时, 0表示头, 1表示第二个元素, 依次类推; index<0时, -1表示尾, -2表示倒数第二个元素, 依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存, 并设置过期时间
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value的数据
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
测试此处省略……
5. Redis配置文件详解
启动的时候, 通过读取配置文件来启动.
单位 units, 对大小写不敏感.
# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf
# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.
包含 INCLUDES
include /path/to/local.conf
include /path/to/other.conf
网络 NETWORK
# 绑定的ip
bind 127.0.0.1 -::1
# 保护模式
protected-mode no
# 端口设置
port 6379
通用 GENERAL
# 以守护进程的方式运行,默认是no,我们需要自己开启为yes
daemonize yes
# 如果以后台方式运行,需要指定一个pid文件
pidfile /var/run/redis_6379.pid
# 日志级别
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 生产环境使用该级别
# warning (only very important / critical messages are logged)
loglevel notice
# 日志文件位置及文件名
logfile ""
# 数据库的数量,默认是16个
databases 16
# 是否总是线上LOGO
always-show-logo no
快照 SNAPSHOTTING, 持久化, 在规定的时间内执行了多少次操作, 则会持久化到.rdb或.aof文件中. redis是内存数据库, 如果没有持久化, 断电后会丢失数据.
# save <seconds> <changes>
# 如果指定时间内,key的修改超过了changes,就会持久化
# 如果900s内, 如果至少有1个key进行了修改, 就进行持久化操作
save 900 1
# 如果300s内, 如果至少有10个key进行了修改, 就进行持久化操作
save 300 10
# 如果60s内, 如果至少有10000个key进行了修改, 就进行持久化操作
save 60 10000
# 持久化如果发生错误,是否还需要继续工作
stop-writes-on-bgsave-error yes
# 是否压缩rdb文件, 需要消耗一些cpu资源
rdbcompression yes
# 保存rdb文件,进行错误的检查校验
rdbchecksum yes
# rdb文件名称
dbfilename dump.rdb
# rdb文件保存目录
dir ./
主从复制
安全 SECURITY, 可以设置redis的密码, 默认是没有密码.
requirepass 123456
客户端修改密码操作
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379> config set requirepass "password123"
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "password123"
127.0.0.1:6379> exit
[root@centos7-01 redis-6.x]# ./bin/redis-cli
127.0.0.1:6379> ping
(error) NOAUTH Authentication required. # 修改密码后ping失败,需要授权
127.0.0.1:6379> auth password123 # 密码授权
OK
127.0.0.1:6379> ping
PONG
限制CLIENTS
# 设置能连接上redis的最大客户端的数量
maxclients 10000
# redis配置最大的内存容量
maxmemory <bytes>
# 内存到达上限之后的处理策略
# volatile-lru -> Evict using approximated LRU, only keys with an expire set. # 从设置了过期时间的key中删除最近很少使用的key
# allkeys-lru -> Evict any key using approximated LRU. 从所有key中删除最近很少使用的key
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set. 从设置了过期时间的key中删除最近使用次数最少的key
# allkeys-lfu -> Evict any key using approximated LFU. 从所有key中删除最近使用次数最少的key
# volatile-random -> Remove a random key having an expire set. 从设置了过期时间的key中随机删除
# allkeys-random -> Remove a random key, any key. 从所有key中随机删除
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL) 剔除即将过期的
# noeviction -> Don't evict anything, just return an error on write operations. 永不剔除
maxmemory-policy noeviction
APPEND ONLY MODE aof模式
appendonly no
appendfilename "appendonly.aof"
#appendfsync always # 每次修改都会sync , 消耗性能
# 每秒执行一次sync, 可能会丢失1s的数据
appendfsync everysec
# appendfsync no # 不执行同步
# 重写时是否可以运行Appendfsync,用默认no即可,保证数据安全性
no-appendfsync-on-rewrite no
# 重写规则, 如果aof文件大于64MB, 会触发aof文件重写
auto-aof-rewrite-percentage 100
# AOF文件大小达到上次rewrite后大小的一倍, 会触发aof文件重写
auto-aof-rewrite-min-size 64mb
6. Redis持久化
Redis是内存数据库, 如果不将内存中的数据库状态保存到磁盘, 那么一旦服务器进程退出, 服务器中的数据库状态也会消失, 所以Redis提供了持久化功能.
6.1 RDB (Redis DataBase)
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是snapshot, 它恢复时是将快照文件读到内存中.
Redis会单独创建(fork)一个子进程来进行持久化, 会先将数据写入到一个临时文件中, 待持久化过后, 再用这个临时文件替换上次持久化好的文件. 整个过程中, 主进程是不进行任何IO操作的, 确保了极高的性能. 如果需要进行大规模的数据恢复, 且对于数据恢复的完整性不是非常敏感, 那RDB方式要比AOF方式更加的高效.
RDB的缺点是最后一次持久化后的数据可能丢失. 默认RDB, 一般情况下不需要修改.
==rdb保存的文件就是dump.rdb==
触发机制
- 满足save规则的情况下, 会自动触发rdb规则.
- 执行flushall命令, 也会触发rdb持久化.
- 退出redis, 也会产生rdb文件.
如何恢复rdb文件的数据
- 只需要将rdb文件放在redis启动目录即可, redis启动的时候会自动检查dump.rdb文件,恢复其中的数据.
- 查看需要存在的位置
# 如果dir配置的目录下存在dump.rdb文件,启动就会自动恢复其中的数据.
127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/redis-6.x"
优点:
- 适合大规模的数据恢复.
- 对数据的完整性要求不高.
缺点:
- 需要一定的时间间隔进行操作. 如果Redis意外宕机了, 最后一次修改数据就没有了.
- fork进程的时候, 会占用一定的内存空间.
6.2 AOF (Append Only File)
将我们的所有命令都记录下来, 恢复数据的时候就把aof文件中的命令全部再执行一遍.
以日志的形式记录每个写操作, 将Redis执行过的所有指令记录下来(读操作不记录), 只许追加文件但不可用改写文件, Redis启动之初会读取该文件重新构建数据, 换言之, Redis重启时会根据日志文件的内容将写指令
从前到后执行一次, 以完成数据的恢复工作.
==aof保存的是appendonly.aof文件==
默认是不打开的, 需要手动开启配置. 重启Redis就可以生效.
appendonly yes
如果aof文件有错误, redis重启会失败, 我们可以通过工具redis-check-aof
修复aof文件.
./bin/redis-check-aof appendonly.aof --fix
优点
appendfsync always
#appendfsync everysec
# appendfsync no
- 每次修改都同步,文件的完整性会更好.
- 每秒同步一次, 可能会丢失一秒的数据
- 从不同步, 效率最高.
缺点
- 相对于数据文件来说, aof远远大于rdb文件, 修复的速度也比rdb慢.
- aof运行效率也要比RDB慢, 所以redis默认的配置是rdb.
6.3 扩展
1). RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储.
2). AOF持久化方式记录每次对服务器写的操作, 当服务器重启的时候会重新执行这些命令来恢复原始的数据, AOF命令以Redis协议追加保存每次写的操作到文件末尾, Redis还能对AOF文件进行后台重写, 使得AOF文件的体积不至于过大.
3). 只做缓存, 如果你只希望你的数据在服务器运行的时候存在, 你也可以不做任何持久化.
4). 同时开启两种持久化方式
- 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集更完整.
- RDB的数据不实时, 同时使用两者时服务器重启也会找AOF文件, 那要不要只使用AOF呢? 建议不这样操作, 因为RDB更适合用于备份数据库(AOF在不断变化, 不适合备份), 快速重启, 而且不会有AOF潜在的BUG.
5). 性能建议
- 因为RDB文件只用作后备用途, 建议只在slave上持久化RDB文件, 而且配置15min备份一次就够了, 只保留
save 900 1
这条规则. - 如果开启 AOF, 好处是在最恶劣情况下也只会丢失不超过两秒的数据, 启动脚本比较简单, 只要load自己的aof文件即可, 代价是带来了持久了IO操作, 并且AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎不可避免. 只要硬盘许可, 应该尽量减少AOF rewrite的频率, AOF重写的基础值默认64MB太小了, 可以设置到5G以上, 默认超过原来aof文件大小的1倍(100%)可以改到更加适当的倍数.
- 如果不开启AOF, 仅靠master-slave replication实现高可用性也可以, 能省掉一大笔IO, 也减少了rewrite时带来的系统波动. 代价是如果master-slave同时宕机, 会丢失十几分钟的数据, 启动脚本也要比较master-slave中的RDB文件, 载入较新的那个RDB文件.
7. Redis发布订阅
Redis发布订阅(pub/sub)是一种消息通信模式: 发送者(pub)发送消息, 订阅者(sub)接收消息.
Redis客户端可以订阅任意数量的频道.
发布/订阅消息图:
下图展示了频道channel1, 以及订阅这个频道的三个客户端: client1, client2 和 client5 之间的关系.
当有新消息通过publish命令发送给频道channel1时, 这个消息就会被发送给订阅它的三个客户端.
这些命令被广泛用于构建即时通信应用, 比如网络聊天室和实时广播, 实时提醒等.
# 订阅给定的一个或多个频道的信息
subscribe channel [channel2 ...]
# 订阅一个或多个符合给定模式的频道
psubscribe pattern [pattern2 ...]
# 将信息发送到指定的频道
publish channel message
# 退订给指定的频道
unsubscribe channel [channel2 ...]
# 退订所有给定模式的频道
punsubscribe pattern [pattern2 ...]
原理
Redis是使用C实现的, 通过分析Redis源码里的pubsub.c文件, 了解发布和订阅机制的底层实现, 加深对Redis的理解, Redis通过PUBLISH, SUBSCRIBE和PSUBSCRIBE等命令实现发布和订阅功能.
通过SUBSCRIBE命令订阅某频道后, redis-server里维护了一个字典, 字典的键就是一个个channel, 而字典的值则是一个链表, 链表中保存了所有订阅这个channel的客户端, SUBSCRIBE命令的关键, 就是将客户端添加到给定的channel的订阅链表中.
通过PUBLISH命令向订阅者发送消息, redis-server会使用给定的频道作为键, 在它所维护的channel字段中查找记录了订阅这个频道的所有客户端的链表, 遍历这个链表, 将消息发布给所有订阅者.
pub/sub从字面上理解就是发布(publish)与订阅(subscribe), 在redis中,你可以设定对某一个key值进行消费发布及消息订阅, 当一个key值上进行了消息发布后, 所有订阅它的客户端都会收到相应的消息. 这一功能最明显的用法就是用作实时消息系统, 比如普通的即时聊天, 群聊等功能.
8. Redis主从复制
8.1 概念
主从复制, 是指将一台Redis服务器的数据复制到其他的Redis服务器, 前者称为主节点(master/leader), 后者称为从节点(slave/follower); 数据的复制是单向的, 只能由主节点到从节点, master以写为主, slave以读为主.
默认情况下, 每台Redis服务器都是主节点, 且一个主节点可以有多个从节点(或没有从节点), 但一个从节点只能有一个主节点.
主从复制的作用主要包括:
- 数据冗余: 主从复制实现了数据的热备份, 是持久化之外的一种数据冗余方式.
- 故障恢复: 当主节点出现问题时, 可以由从节点提供服务, 实现快速的故障恢复, 实际上是一种服务的冗余.
- 负载均衡: 在主从复制的基础上, 配合读写分离, 可以由主节点提供写服务, 由从节点提供读服务(即写Redis数据时应用连接主节点, 读Redis数据时应用连接从节点), 分担服务器负载; 尤其是在写少读多的场景下, 通过多个从节点分担读负载, 可以大大提高Redis服务器的并发量.
- 高可用基石: 除了上述作用外, 主从复制还是哨兵和集群能够实施的基础, 因此说主从复制是Redis高可用的基础.
一般来说, 要将Redis运用于工程项目中, 只使用一台Redis是万万不能的, 原因如下:
- 从结构上, 单个Redis服务器会发生单点故障, 并且一台服务器需要处理所有的请求负载, 压力较大.
- 从容量上, 单个Redis服务器内存容量有限, 就算一台Redis服务器内存容量为256G, 也不能将所有内存用作Redis存储内存. ==一般单台Redis最大使用内存不应该超过20G.==
电商网站上的商品, 一般都是一次上传, 无数次浏览的, 说专业点也就是”多读少写”.
环境配置
只配置从库, 不配置主库.
127.0.0.1:6379> info replication # 查看当前库的信息
# Replication
role:master # 角色
connected_slaves:0 # 连接的从机数
master_failover_state:no-failover
master_replid:16021c9d9e112e153d4201fbed78b95ae5c500ae
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
主从复制配置文件修改
# 关闭保护模式,用于公网访问
protected-mode no
# 修改端口
port 6380
# 后台启动
daemonize yes
pidfile /var/run/redis_6380.pid
# 防止在其他目录启动,最好写绝对路径的文件名 /usr/local/redis-6.x/data/6380.log
logfile "./data/6380.log"
# 此处绑定ip可以是内网ip和本机ip, 也可以直接注释掉该项
# bind 127.0.0.1
# 用于连接主节点密码
masterauth 123456
# 设置redis密码, 各个节点请保持密码一致
requirepass 123456
# 修改rdb文件名称
dbfilename dump_6380.rdb
# 数据备份文件的目录; 日志文件的默认目录等; 防止在其他目录启动,最好写绝对路径
dir /usr/local/redis-6.x/data
8.2 一主二从
默认情况下, 每台redis服务器都是主节点, 我们一般只需要配置从机就可以了. 配置从机:
# 关闭保护模式,用于公网访问
protected-mode no
# 修改端口
port 6380
# 后台启动
daemonize yes
pidfile /var/run/redis_6380.pid
# 防止在其他目录启动,最好写绝对路径的文件名 /usr/local/redis-6.x/data/6380.log
logfile "./data/6380.log"
# 此处绑定ip可以是内网ip和本机ip, 也可以直接注释掉该项
# bind 127.0.0.1
# 用于连接主节点密码
masterauth 123456
# 设置redis密码, 各个节点请保持密码一致
requirepass 123456
# 修改rdb文件名称
dbfilename dump_6380.rdb
# 数据备份文件的目录; 日志文件的默认目录等; 防止在其他目录启动,最好写绝对路径
dir /usr/local/redis-6.x/data
首先分别启动三台Redis服务, 6379, 6380 , 6381
[root@centos7-01 ~]# cd /usr/local/redis-6.x/
[root@centos7-01 redis-6.x]# ll
total 384
-rwxr-xr-x. 1 root root 4514 Apr 7 22:41 appendonly.aof
drwxr-xr-x. 2 root root 150 Jan 17 21:49 bin
drwxr-xr-x. 2 root root 139 Apr 6 21:58 data
-rw-r--r--. 1 root root 142 Apr 14 22:23 dump.rdb
-rwxr-xr-x. 1 root root 93825 Apr 4 18:00 redis6379.conf
-rwxr-xr-x. 1 root root 93825 Apr 4 18:01 redis6380.conf
-rwxr-xr-x. 1 root root 93825 Apr 4 18:01 redis6381.conf
-rwxr-xr-x. 1 root root 93745 Apr 3 21:06 redis.conf
-rw-r--r--. 1 root root 315 Apr 5 16:01 sentinel.conf
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6379.conf
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6380.conf
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6381.conf
[root@centos7-01 redis-6.x]#
[root@centos7-01 redis-6.x]# ps -ef | grep redis-server
root 1587 1 0 21:31 ? 00:00:00 ./bin/redis-server *:6379
root 1594 1 0 21:31 ? 00:00:00 ./bin/redis-server *:6380
root 1600 1 0 21:31 ? 00:00:00 ./bin/redis-server *:6381
分别连接上三台redis服务的客户端, 通过 info replication
查看集群信息.
# 6379
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:27d733e907c3f00821f7373b605225bf8315a171
master_replid2:0000000000000000000000000000000000000000
// 省略......
# 6380
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6380 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6380> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:da1daf77cf23159d911a33fc1f4f82ed2d3dd374
master_replid2:0000000000000000000000000000000000000000
// 省略......
# 6381
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6381 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6381> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:e8ed1c0ea76f7e022327acdc56207b9aafd75bb4
master_replid2:0000000000000000000000000000000000000000
// 省略......
从上面的打印信息可以看出, 刚启动的三台Redis服务都是独立的master, 之间没有建立主从关系. 下面我们以6379为master, 与6380, 6381建立主从关系.
# slave-6380
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6380> INFO replication
# Replication
role:slave # 角色
master_host:127.0.0.1 # 主机
master_port:6379 # 端口
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_read_repl_offset:14
slave_repl_offset:14
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:eeb823aa531ef123e24f83f918a680f2d481e5b5
master_replid2:0000000000000000000000000000000000000000
// 省略......
# slave-6381
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:56
slave_repl_offset:56
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
// 省略......
# master-6379
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2 # 可以看到两台slave连接
slave0:ip=127.0.0.1,port=6380,state=online,offset=56,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=56,lag=1
// 省略......
主机可以写, 从机不能写, 从机只能读, 主机的所有信息和数据都会同步到从机.
# 6379 master
127.0.0.1:6379> keys *
1) "class"
2) "k2"
3) "k1"
127.0.0.1:6379> set k1 v1 # master可以写
OK
127.0.0.1:6379> get k1
"v1"
# 6380 slave
127.0.0.1:6380> keys *
1) "k2"
2) "class"
3) "k1"
127.0.0.1:6380> set k1 v1 # slave不能写操作
(error) READONLY You can't write against a read only replica.
127.0.0.1:6380> get k1 # slave可以读
"v1"
注意 :
(1) 主机断开连接, 从机依然连接到主机的, 但是没有写操作, 如果主机重新启动恢复, 从机依然可以直接获取主机写的数据.
(2) 如果从机宕机后, 从机重启后会变成独立的master, 如果要获取之前主机的数据, 需要重新与之前的主机建立主从关系.(客户端命令方式才会这样, 配置文件配置了主从关系不会出现该情况)
==复制原理==
slave启动成功后, 连接到master会发送一个sync同步命令, Master接收到命令, 会启动后台的存盘进程, 同时收集所有接收到的用于修改数据集的命令, 在后台进程执行完毕后, master将传送整个数据文件到slave, 并完成一次完整的同步.
全量复制: slave服务在接收到数据文件后, 将其存盘并加载到内存中.
增量复制: Master继续将新的所有收集到的修改命令依次传给slave, 完成同步.\
只要是重新连接到master, 会触发一次全量复制的完整同步.
真实的主从配置应该是在配置文件中, 这样才是永久的, 上面的方式只是暂时的.
################################## REPLICATION #################################
replicaof <masterip> <masterport>
masterauth "123456"
8.3 层层链路
层层链路模型也可以完成数据的主从复制, 例如: 6379是6380的master, 6380是6381的master, 如果6379宕机了, 6380依然是slave, 不能写操作. 我们平时并不使用这种模式.
如果master宕机了, 没有主机了, 此时6380和6381都是从机, 可以命令slaveof no one
手动设置来让自己成为master. 其他slave就可以自动连接到这个新的master.
# 将6381服务关闭, 然后重启,建立与6380的主从关系. 6379是6380的master, 6380是6381的master.
127.0.0.1:6381> SHUTDOWN
not connected> exit
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6381.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6381 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6381> SLAVEOF 127.0.0.1 6380
OK
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380 # 6380是6381的master.
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_read_repl_offset:4519
slave_repl_offset:4519
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
// 省略......
此时将master-6379宕机, 没有了master, 6380依然是slave, 无法写操作.
# 6379宕机
127.0.0.1:6379> SHUTDOWN
not connected>
# 6380-slave 写操作报错
127.0.0.1:6380> set k4 v4 # slave不能写操作
(error) READONLY You can't write against a read only replica.
使用命令slaveof no one
手动设置6380为master主机, 完成写操作.
# 6380变为master
127.0.0.1:6380> SLAVEOF no one
OK
127.0.0.1:6380> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=4589,lag=0
master_failover_state:no-failover
master_replid:2074cd7c6402435e472b2bd2d0b11ee0b6b3abf1
master_replid2:eeb823aa531ef123e24f83f918a680f2d481e5b5
// 省略......
127.0.0.1:6380>
127.0.0.1:6380> set k4 v4 # 6380变成master后,完成了写操作
OK
# 6381读取到6380新写的数据
127.0.0.1:6381> get k4
"v4"
8.4 哨兵模式
上面的一主二从和层层链路都不是我们实际工作中使用的, 工作中是搭建的高可用的哨兵+集群模式, 当master宕机后, 哨兵会进行选举, 自动从slave中选出一个服务作为新的master, 而且哨兵也是集群的.
概述
主从切换: 当主服务器宕机后, 需要手动把一台从服务器切换为主服务器, 这样人工干预费时费力, 还会造成一段时间内的服务不可用, 不推荐. 更多时候, 我们会推荐使用哨兵模式, Redis2.8版本开始正式提供了Sentinel(哨兵)架构来解决这个问题.
哨兵模式, 首先Redis提供了哨兵的命令, 哨兵是一个独立的进程, 会独立运行, 原理是哨兵通过发送命令, 等待redis服务器响应, 从而达到监控多个运行的Redis实例.
哨兵的两个作用:
- 通过发送命令, 让redis服务器返回监控的redis实例运行状态, 包括主服务器和从服务器.
- 当哨兵检测到master宕机, 会自动将slave切换成master, 然后通过
发布订阅模式
通知其他的从服务器, 修改配置文件, 让它们切换主机.
然而一个哨兵进程对Redis服务进行监控, 也可能出现问题. 为此, 可以使用多个哨兵进行监控, 各个哨兵之间还会进行监控, 这样就形成了多哨兵模式
.
假设主服务器宕机, 哨兵1线检测到这个结果, 系统并不会马上进行failover过程, 仅仅是哨兵1主观的认为master不可用, 这个现象称为主观下线
, 当后面的哨兵也检测到主服务器master不可用, 并且数量达到一定数值时, 那么哨兵之间就会进行一次投票, 投票的结果由一个哨兵发起, 进行failover[故障转移]操作, 选举新master成功后, 就会同步发布订阅模式, 让各个哨兵把自己监控的slave从服务器切换它们的主master, 这个过程称为客观下线
.
一主二从的分配,来搭建哨兵.
配置哨兵配置文件 sentinel.conf
# sentinel monitor 监控的服务名称 host port 数值
sentinel monitor redis6379 127.0.0.1 6379 1
# 数值1, 表示如果master主机宕机, 哨兵进行投票,master是否真的宕机了 票数最多的(达到配置的数值),就会重新选举出新的master.
启动哨兵进程
[root@centos7-01 redis-6.x]# ps -ef|grep redis-server
root 1594 1 0 21:31 ? 00:00:24 ./bin/redis-server *:6380 # slave
root 2074 1 0 22:34 ? 00:00:08 ./bin/redis-server *:6381 # slave
root 2095 1 0 22:45 ? 00:00:08 ./bin/redis-server *:6379 # master
root 2156 1908 0 23:10 pts/3 00:00:00 grep --color=auto redis-server
[root@centos7-01 redis-6.x]#
[root@centos7-01 redis-6.x]# ./bin/redis-sentinel sentinel.conf # 启动哨兵进程
2159:X 20 Apr 2022 23:10:55.521 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2159:X 20 Apr 2022 23:10:55.521 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=2159, just started
2159:X 20 Apr 2022 23:10:55.521 # Configuration loaded
2159:X 20 Apr 2022 23:10:55.521 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2159:X 20 Apr 2022 23:10:55.521 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.2.6 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2159
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
2159:X 20 Apr 2022 23:10:55.522 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2159:X 20 Apr 2022 23:10:55.522 # Sentinel ID is c34ca410b69366f5ec74d2e0b9e93e0213c94245
2159:X 20 Apr 2022 23:10:55.522 # +monitor master redis6379 127.0.0.1 6379 quorum 1 # 哨兵监控master 6379
如果此时, Master节点宕机, 哨兵会从slave中重新选举出一个新的master.
# 6379 master
127.0.0.1:6379> INFO replication
# Replication
role:master # master
connected_slaves:2 # 2个slave连接 6380 6381
slave0:ip=127.0.0.1,port=6380,state=online,offset=2086,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=2086,lag=0
master_failover_state:no-failover
master_replid:20b15024cdde0f00205909954c6324972cc470fe
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2086
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2086
# 6379 宕机
127.0.0.1:6379> SHUTDOWN
等待一段时间, 查看哨兵日志, 进行了选举操作.
2159:X 20 Apr 2022 23:11:25.601 # +sdown master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.601 # +odown master redis6379 127.0.0.1 6379 #quorum 1/1
2159:X 20 Apr 2022 23:11:25.601 # +new-epoch 50
2159:X 20 Apr 2022 23:11:25.601 # +try-failover master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.606 # +vote-for-leader c34ca410b69366f5ec74d2e0b9e93e0213c94245 50
2159:X 20 Apr 2022 23:11:25.606 # +elected-leader master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.606 # +failover-state-select-slave master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.700 # -failover-abort-no-good-slave master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.769 # Next failover delay: I will not start a failover before Wed Apr 20 23:17:26 2022
???? 选举没有成功, 不知道咋回事啊, 正常应该会从6380 , 6381中选举一个新的master
上面哨兵模式下, 选举master失败, 经过查阅资料发现了问题 , 原来是我主从服务器都设置了密码, 哨兵的配置文件也要进行密码授权, 才能正常监听到redis服务的状态.
# 再次修改sentinel.conf配置文件
sentinel monitor redis6379 127.0.0.1 6379 1
# 和master的密码保持一致
sentinel auth-pass redis6379 123456
再次启动哨兵进程, 启动redis6379
[root@centos7-01 redis-6.x]# ./bin/redis-sentinel sentinel.conf
2220:X 20 Apr 2022 23:23:24.469 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2220:X 20 Apr 2022 23:23:24.469 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=2220, just started
2220:X 20 Apr 2022 23:23:24.469 # Configuration loaded
2220:X 20 Apr 2022 23:23:24.469 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2220:X 20 Apr 2022 23:23:24.469 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.2.6 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2220
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
2220:X 20 Apr 2022 23:23:24.470 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2220:X 20 Apr 2022 23:23:24.470 # Sentinel ID is 0c300a1bceb96652a7ac63e821ea187f1d283e0e
2220:X 20 Apr 2022 23:23:24.470 # +monitor master redis6379 127.0.0.1 6379 quorum 1 # 哨兵监控6379 master
2220:X 20 Apr 2022 23:23:45.009 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379 # 6380 salve
2220:X 20 Apr 2022 23:23:45.015 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6379 # 6381 salve
然后再次重现上面的步骤, master6379宕机, 等待一会儿, 查看哨兵日志, 已经完成了自动选举, 6380被选为新的master.
# 6379 master宕机
2220:X 20 Apr 2022 23:24:45.998 # +sdown master redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:45.999 # +odown master redis6379 127.0.0.1 6379 #quorum 1/1
# 哨兵发起命令, 2个slave之间进行选举
2220:X 20 Apr 2022 23:24:45.999 # +new-epoch 2
2220:X 20 Apr 2022 23:24:45.999 # +try-failover master redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:46.006 # +vote-for-leader 0c300a1bceb96652a7ac63e821ea187f1d283e0e 2
2220:X 20 Apr 2022 23:24:46.006 # +elected-leader master redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:46.006 # +failover-state-select-slave master redis6379 127.0.0.1 6379
# 选举6380
2220:X 20 Apr 2022 23:24:46.107 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:46.107 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:46.181 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:47.149 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:47.149 # +failover-state-reconf-slaves master redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:47.215 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:48.154 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:48.155 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:48.207 # +failover-end master redis6379 127.0.0.1 6379
# 最终6380替换6379成为新的master
2220:X 20 Apr 2022 23:24:48.207 # +switch-master redis6379 127.0.0.1 6379 127.0.0.1 6380
2220:X 20 Apr 2022 23:24:48.208 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6380
2220:X 20 Apr 2022 23:24:48.208 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis6379 127.0.0.1 6380
2220:X 20 Apr 2022 23:25:18.282 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis6379 127.0.0.1 6380 # 检测6379还是down
我们通过查看6380, 已经是master. 并且6381的配置改写, 认6380为master.
# 6380 master
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=4968,lag=1
master_failover_state:no-failover
master_replid:8f83123fd97417dec1455dbc558f79d9caf654ae
master_replid2:5961664a5c896ed322afd82caac43210c677fb35
// ...
### 6381 slave , 认6380为master
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_read_repl_offset:6068
slave_repl_offset:6068
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:8f83123fd97417dec1455dbc558f79d9caf654ae
master_replid2:5961664a5c896ed322afd82caac43210c677fb35
// ...
注意: 如果此时6379重新启动后, 也不会成为master, 会以6380为master.
# 6379复活
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6379.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6379 -a 123456
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_read_repl_offset:64499
slave_repl_offset:64499
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:8f83123fd97417dec1455dbc558f79d9caf654ae
master_replid2:0000000000000000000000000000000000000000
# 查看6380, 连接了2个salve, 其中一个是复活的6379
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6381,state=online,offset=67637,lag=0
slave1:ip=127.0.0.1,port=6379,state=online,offset=67637,lag=0 # slave 6379
master_failover_state:no-failover
master_replid:8f83123fd97417dec1455dbc558f79d9caf654ae
master_replid2:5961664a5c896ed322afd82caac43210c677fb35
master_repl_offset:67637
second_repl_offset:2760
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:67637
# 6379复活了, 只能是slave, 认6380为master
2220:X 20 Apr 2022 23:38:10.334 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis6379 127.0.0.1 6380
2220:X 20 Apr 2022 23:38:20.299 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis6379 127.0.0.1 6380
优点:
- 哨兵集群, 基于主从复制模式, 所有的主从配置优点, 它全有.
- 主从可以切换, 故障可以转移, 系统的可用性就更好.
- 哨兵模式就是主从模式的升级, 手动到自动, 更加健壮.
缺点:
- Redis不方便在线扩容, 集群容量一旦达到上限, 在线扩容十分麻烦.
- 实现哨兵模式的配置有很多选择, 麻烦.
哨兵模式的全部配置
# 哨兵实例运行的端口, 默认26379
port 26379
# 哨兵的工作目录
dir /tmp
# 哨兵监控的Redis主节点的ip,port
# master-name 可以自己命名的主节点名字, 只能由字母A-z,数字0-9, 以及这三个字符".-_"组成
# quorum 配置的多少个哨兵, 统一认为master主节点失联, 就认为master真的失联.
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379
# 当在Redis实例中开启了requirepass foobared 授权密码, 那么所有连接Redis实例的客户端都要提供密码.
# 设置哨兵连接主从的密码, 必须和主从设置一致的密码.
sentinel auth-pass mymaster passwd
# 指定多少毫秒后, 主节点没有应答哨兵sentinel, 哨兵就主观认为主节点下线, 默认30s.
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 指定在发生failover主备切换时, 最多可以有多少个slave同时对新的master进行同步.
# 配置的数字越小, 完成failover的时间就越长.
# 如果数字越大, 意味着多的slave因为replication而不可用.
# 可以通过将这个值设置为1来保证每次只有一个slave处于不能处理命令请求的状态.
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout可以用在以下这些方面.
# 1.同一个sentinel对同一个master两次failover之间的时间间隔.
# 2.当一个slave从一个错误的master同步数据开始计算时间, 直到slave被纠正为向正确的master同步数据为止.
# 3.想要取消一个正在进行的failover所需的时间.
# 4.当进行failover时, 配置所有slaves指向新的master所需的最大时间, 即时过了这个时间, slaves依然会被正确配置为指向master, 但是就不按照
# parallel-syncs的规则来了.
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
# 配置当某一事件发生时所需要执行的脚本, 可以通过脚本来通知管理员, 例如当系统运行不正确时发送邮件通知相关人员.
# 对于脚本的运行结果有如下规则:
# 若脚本执行后返回1, 那么该脚本稍后将会被再次执行, 重复次数目前默认为10
# 若脚本执行后返回2, 或者比2更高的一个返回值, 脚本将不会重复执行.
# 如果脚本在执行过程中, 由于收到系统中断信号被终止了, 则同返回值为1时的行为相同.
# 一个脚本的最大执行时间为60s, 如果超过这个时间, 脚本将会被一个sigkill信号终止, 之后重新执行.
# 通知型脚本: 当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等), 将会去调用这个脚本,这时这个脚本应该通过邮件, SMS等方式去通知系统管理员关于系统不正常运行的信息, 调用该脚本时, 将传给脚本两个参数, 一个是事件的类型, 一个是事件的描述. 如果sentinel.conf配置文件中配置了这个脚本路径, 那么必须保证这个脚本存在于这个路径并且是可执行的,否则sentile无法正常启动成功.
# 通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
9. Redis缓存穿透和雪崩
Redis缓存的使用极大的提升了应用程序的性能和效率, 特别是数据查询方面, 但同时, 它也带来了一些问题, 其中最重要的问题就是数据一致性问题
, 如果对数据一致性的要求很高, 那么就不能使用缓存,而是使用关系型数据库.
另外的一些典型的问题, 缓存穿透, 缓存雪崩和缓存击穿. 目前业界也有比较好的解决方案.
9.1 缓存穿透(查不到)
概念
用户查询一个key的数据, 发现redis缓存中没有, 持久层数据库查询也没有导致本次查询失败, 当这种请求很多时, 缓存都没有命中, 查询请求全部打在了持久层数据库上, 会给我们的持久层数据库带来很大的压力, 这种现象就是缓存穿透.
解决方案
==布隆过滤器==
布隆过滤器是一种数据结构, 对所有可能查询的参数以hash形式存储, 在控制层进行校验, 不符合则丢弃, 从而避免了对底层存储系统的查询压力.
==缓存空对象==
当存储层不命中后, 即使返回的空对象也将其缓存起来, 同时会设置一个过期时间 , 之后再访问这个数据将会从缓存中获取, 保护了后端的数据源.
但是这种方法会存在两个问题:
- 如果空值能够被缓存起来, 这意味着缓存需要更多的空间存储更多的键, 因为可能会有大量的空值的键.
- 即使对空值设置了过期时间, 还是会存在缓存层和存储层的数据会有一段时间窗口的不一致, 这对需要保持一致性的业务会有影响.
9.2 缓存击穿(高并发,缓存过期)
概述
一个非常热点的key, 并发集中对这一个点进行访问, 当这个key在失效的瞬间, 持续的并发就会击穿缓存, 直接请求到数据库, 就像在一个屏障上凿开了一个洞一样.
当某个key在过期的瞬间, 有大量的请求并发访问, 这类数据一般是热点数据, 由于缓存过期, 会同时访问数据库来查询最新数据, 并且回写缓存, 会导致数据库瞬间压力过大而崩溃.
解决方案
==设置热点数据永不过期==
从缓存层面来看, 没有设置过期时间, 所以不会出现热点key过期后产生的问题.
==加互斥锁==
使用分布式锁, 保证对每个key同时只有一个线程去查询后端数据库服务, 其他线程没有获取到分布式锁的权限, 就会阻塞等待. 这样会把高并发的压力转移到分布式锁上, 因此对分布式锁的考验很大.
9.3 缓存雪崩
概念
是指在某一个时间段, 缓存集中过期失效或Redis宕机. 此时高并发的查询请求过来, 缓存无法命中, 所有查询请求打在了持久层数据库上, 造成持久层数据库的崩溃.
集中过期并不是非常致命的, 更致命的缓存雪崩是当缓存服务器某个节点宕机或断网. 因为自然形成的缓存雪崩, 一定是在某个时间段集中创建缓存, 这个时候数据库也是可以顶住压力的, 无非就是对数据库产生周期性的压力而已. 而缓存服务器节点的宕机, 对数据库服务器造成的压力是不可预知的, 很有可能瞬间就把数据库压崩塌.
解决方案
==Redis高可用==
Redis存在宕机风险, 可以横向扩展, 多加几台Redis节点, 这样一台Redis宕机后, 其他的节点还是可以继续工作, 也就是搭建高可用集群.(异地多活)
==限流降级==
在缓存失效后, 通过加锁或者队列来控制读数据库写缓存的线程数量, 比如: 对某个key只允许一个线程查询数据和写缓存, 其他线程等待.
==数据预热==
在服务正式部署之前, 把可能命中的数据预先访问一遍, 这样部分可能高并发请求的数据会先被加载到缓存中.
在即将发生大并发访问之前,手动触发加载不同的key到缓存中, 并设置不同的过期时间, 让缓存失效的时间点尽量均匀分布.