个人博客
欢迎访问个人博客: https://www.crystalblog.xyz/
备用地址: https://wang-qz.gitee.io/crystal-blog/
1. 简介
Redis前面的基础部分此处不做记录 , 本篇记录狂神说讲解的Redis配置及高级应用的知识.
Redis官网
Redis中文网
https://redis.com.cn/
http://www.redis.cn/
https://www.redis.net.cn/
2. 特殊数据类型
2.1 geospatial 地理位置
朋友的定位, 附近的人, 打车距离计算?
Redis3.2版本新增了Geo. 这个功能可以推算地理位置的信息, 两地之间的距离, 方圆多少距离内的人等.
geoadd 添加地理位置
规则: 两级无法直接添加, 我们一般会下载城市数据, 直接通过java程序一次性导入
有效的经度从-180度到180度。 有效的纬度从-85.05112878度到85.05112878度。 当坐标位置超出上述指定范围时,该命令将会返回一个错误。城市经纬度查询网站
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
返回两个给定位置之间的距离。如果两个位置之间的其中一个不存在, 那么命令返回空值。
如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。
指定单位的参数 unit 必须是以下单位的其中一个:
- m 表示单位为米。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
127.0.0.1:6379> GEODIST china:city beijing shanghai
"4132638.7228"
127.0.0.1:6379> GEODIST china:city beijing shanghai km
"4132.6387"
我附近的人? (获取附近人的地址, 定位), 通过半径来查询.
georadius
以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
范围可以使用以下其中一个单位:
- 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
1) "chongqing"
2) "shenzhen"
3) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km
1) "chongqing"
127.0.0.1:6379> GEORADIUS china:city 110 30 800 km withdist
1) 1) "chongqing"
2) "341.9374"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist count 2
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
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
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%误差, 可以忽略不计.
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
(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
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2
OK
127.0.0.1:6379> PFCOUNT 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单条命令是保证原子性的, 但是事务是不保证原子性的.
事务操作命令:
开启事务并执行成功
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
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
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的乐观锁操作.
127.0.0.1:6379> WATCH 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
(nil)
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
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
OK
127.0.0.1:6379> WATCH 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:
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")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
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) {
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.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Object.class));
redisTemplate.setHashKeySerializer(stringRedisSerializer);
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;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
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;
}
}
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
public boolean hashKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
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));
}
}
}
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
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;
}
}
public long incr(String key, long increment) {
if ((increment < 0)) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, increment);
}
public long decr(String key, long decrement) {
if (decrement < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -decrement);
}
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return 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;
}
}
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;
}
}
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;
}
}
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
public boolean hHashkey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
public double hincr(String key, String item, double increment) {
return redisTemplate.opsForHash().increment(key, item, increment);
}
public double hdecr(String key, String item, double decrement) {
return redisTemplate.opsForHash().increment(key, item, -decrement);
}
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
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;
}
}
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
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;
}
}
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
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;
}
}
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
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;
}
}
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;
}
}
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, 对大小写不敏感.
包含 INCLUDES
include /path/to/local.conf
include /path/to/other.conf
网络 NETWORK
bind 127.0.0.1 -::1
protected-mode no
port 6379
通用 GENERAL
daemonize yes
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
always-show-logo no
快照 SNAPSHOTTING, 持久化, 在规定的时间内执行了多少次操作, 则会持久化到.rdb或.aof文件中. redis是内存数据库, 如果没有持久化, 断电后会丢失数据.
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.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]
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth password123
OK
127.0.0.1:6379> ping
PONG
限制CLIENTS
maxclients 10000
maxmemory <bytes>
maxmemory-policy noeviction
APPEND ONLY MODE aof模式
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
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文件,恢复其中的数据.
- 查看需要存在的位置
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
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
logfile "./data/6380.log"
masterauth 123456
requirepass 123456
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 ~]
[root@centos7-01 redis-6.x]
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]
[root@centos7-01 redis-6.x]
[root@centos7-01 redis-6.x]
[root@centos7-01 redis-6.x]
[root@centos7-01 redis-6.x]
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 查看集群信息.
[root@centos7-01 redis-6.x]
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
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:27d733e907c3f00821f7373b605225bf8315a171
master_replid2:0000000000000000000000000000000000000000
// 省略......
[root@centos7-01 redis-6.x]
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
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:da1daf77cf23159d911a33fc1f4f82ed2d3dd374
master_replid2:0000000000000000000000000000000000000000
// 省略......
[root@centos7-01 redis-6.x]
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
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:e8ed1c0ea76f7e022327acdc56207b9aafd75bb4
master_replid2:0000000000000000000000000000000000000000
// 省略......
从上面的打印信息可以看出, 刚启动的三台Redis服务都是独立的master, 之间没有建立主从关系. 下面我们以6379为master, 与6380, 6381建立主从关系.
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6380> INFO 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
// 省略......
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6381> INFO 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
// 省略......
127.0.0.1:6379> INFO replication
role:master
connected_slaves:2
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
// 省略......
主机可以写, 从机不能写, 从机只能读, 主机的所有信息和数据都会同步到从机.
127.0.0.1:6379> keys *
1) "class"
2) "k2"
3) "k1"
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6380> keys *
1) "k2"
2) "class"
3) "k1"
127.0.0.1:6380> set k1 v1
(error) READONLY You can't write against a read only replica.
127.0.0.1:6380> get k1
"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.
127.0.0.1:6381> SHUTDOWN
not connected> exit
[root@centos7-01 redis-6.x]
[root@centos7-01 redis-6.x]
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
role:slave
master_host:127.0.0.1
master_port:6380
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, 无法写操作.
127.0.0.1:6379> SHUTDOWN
not connected>
127.0.0.1:6380> set k4 v4
(error) READONLY You can't write against a read only replica.
使用命令slaveof no one 手动设置6380为master主机, 完成写操作.
127.0.0.1:6380> SLAVEOF no one
OK
127.0.0.1:6380> INFO 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
OK
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]
root 1594 1 0 21:31 ? 00:00:24 ./bin/redis-server *:6380
root 2074 1 0 22:34 ? 00:00:08 ./bin/redis-server *:6381
root 2095 1 0 22:45 ? 00:00:08 ./bin/redis-server *:6379
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]
2159:X 20 Apr 2022 23:10:55.521
2159:X 20 Apr 2022 23:10:55.521
2159:X 20 Apr 2022 23:10:55.521
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
2159:X 20 Apr 2022 23:10:55.522
2159:X 20 Apr 2022 23:10:55.522
如果此时, Master节点宕机, 哨兵会从slave中重新选举出一个新的master.
127.0.0.1:6379> INFO replication
role:master
connected_slaves:2
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
127.0.0.1:6379> SHUTDOWN
等待一段时间, 查看哨兵日志, 进行了选举操作.
2159:X 20 Apr 2022 23:11:25.601
2159:X 20 Apr 2022 23:11:25.601
2159:X 20 Apr 2022 23:11:25.601
2159:X 20 Apr 2022 23:11:25.601
2159:X 20 Apr 2022 23:11:25.606
2159:X 20 Apr 2022 23:11:25.606
2159:X 20 Apr 2022 23:11:25.606
2159:X 20 Apr 2022 23:11:25.700
2159:X 20 Apr 2022 23:11:25.769
???? 选举没有成功, 不知道咋回事啊, 正常应该会从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]
2220:X 20 Apr 2022 23:23:24.469
2220:X 20 Apr 2022 23:23:24.469
2220:X 20 Apr 2022 23:23:24.469
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
2220:X 20 Apr 2022 23:23:24.470
2220:X 20 Apr 2022 23:23:24.470
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
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
然后再次重现上面的步骤, master6379宕机, 等待一会儿, 查看哨兵日志, 已经完成了自动选举, 6380被选为新的master.
2220:X 20 Apr 2022 23:24:45.998
2220:X 20 Apr 2022 23:24:45.999
2220:X 20 Apr 2022 23:24:45.999
2220:X 20 Apr 2022 23:24:45.999
2220:X 20 Apr 2022 23:24:46.006
2220:X 20 Apr 2022 23:24:46.006
2220:X 20 Apr 2022 23:24:46.006
2220:X 20 Apr 2022 23:24:46.107
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
2220:X 20 Apr 2022 23:24:47.149
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
2220:X 20 Apr 2022 23:24:48.207
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
我们通过查看6380, 已经是master. 并且6381的配置改写, 认6380为master.
127.0.0.1:6380> info 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
// ...
127.0.0.1:6381> INFO 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.
[root@centos7-01 redis-6.x]
[root@centos7-01 redis-6.x]
127.0.0.1:6379> INFO 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
127.0.0.1:6380> info 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
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
2220:X 20 Apr 2022 23:38:10.334
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到缓存中, 并设置不同的过期时间, 让缓存失效的时间点尽量均匀分布.
|