Redis
Redis 是一种Key-Value类型的缓存数据库
Redis源码地址址 https://github.com/redis/redis
Redis在线测试 https://try.redis.io
Redis命令参考 http://doc.redisfans.com/
Linux安装Redis
Linux环境安装Redis必须先具备gcc编译环境
查看 gcc -V
安装redis之前需要具备c++库环境 yum -y install gcc-c++
官网下载压缩包放到/opt目录 解压tar -zxvf redis-7.0.0.targz
在redis-7.0.0目录下执行make命令 make && make install
查看默认安装目录: usr/local/bin
在/opt的redis目录下 将默认的redis.conf拷贝到自己定义好的一个路径下,比如/myredis
修改/myredis目录下redis.conf配置文件做初始化设置
redis.conf配置文件,改完后确保生效,记得重启,记得重启
1 默认daemonize no 改为 daemonize yes
2 默认protected-mode yes 改为 protected-mode no
3 默认bind 127.0.0.1 改为 直接注释掉(默认bind 127.0.0.1只能本机访问)或改成本机IP地址,否则影响远程IP连接
4 添加redis密码 改为 requirepass 你自己设置的密码
/usr/local/bin目录下运行redis-server,启用/myredis目录下的redis.conf文件
关闭
单实例关闭: shutdown
多实例关闭,指定端口关闭:redis-cli -p 6379 shutdown
删除/usr/local/lib目录下与redis相关的文件 删除前先停止服务 redis-server 服务
ls -l /usr/local/bin/redis-*
rm -rf /usr/local/bin/redis-*
Resit十大数据类型
1.redis字符串 (String)
string是redis最基本的类型,一个key对应一个value。
string类型是二进制安全的,意思是redis的string可以包含任何数据,比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个redis中字符串value最多可以是512M
2.redis列表 (List)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
它的底层实际是个双端链表,最多可以包含 2^32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)
3.redis哈希表 (Hash)
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
Redis 中每个 hash 可以存储 2^32 - 1 键值对(40多亿)
4. redis集合(Set)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,集合对象的编码可以是 intset 或者 hashtable。
Redis 中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)
5. redis有序集合 (ZSet)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数,redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
zset集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 2^32 - 1
6.redis地理空间 (GEO)
Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,包括
添加地理位置的坐标。
获取地理位置的坐标。
计算两个位置之间的距离。
根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
7.redis基数统计 (HyperLogLog)
HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
8.redis位图 (bitmap)
由0和1状态表现的二进制位的bit数组
9. redis位域 (bitfield)
通过bitfield命令可以一次性操作多个比特位域(指的是连续的多个比特位),它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应操作的执行结果。
就是通过bitfield命令我们可以一次性对多个比特位域进行操作。
10.redis流 (Stream)
Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。
而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失
获得redis常见数据类型操作命令
官网英文:https://redis.io/commands/
中文:http://www.redis.cn/commands.html
常用Redis命令
keys * 查看当前库所有的key
exists key 判断某个key是否存在
type key 查看你的key是什么类型
del key 删除指定的key数据
unlink key非阻塞删除,仅仅将keys从keyspace元数据中删除,真正的删除会在后续异步中操作。
ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
expire key 秒钟为给定的key设置过期时间
move key dbindex (0-15] 将当前数据库的 key移动到给定的数据库 db 当中
select dbindex 切换数据库[0-15],默认为
dbsize 查看当前数据库key的数量
flushdb 清空当前库
flushall 通杀全部库
Redis持久化
RDB (Redis DataBase)
实现类似照片记录效果的方式,就是把某一时刻的数据和状态以文件的形式写到磁盘上,也就是快照。这样一来即使故障宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为RDB文件(dump.rdb),其中,RDB就是Redis DataBase的缩写。
Redis7的RDB默认备份策略与7之前的默认策略有差异
自动触发 (修改conf文件)
修改触发条件为5秒2次修改则自动备份
修改dump文件保存路径
默认路径为:./
自定义修改的路径且可以进入redis里用CONFIG GET dir获取目录
修改dump文件名称
如何恢复
将备份文件(dump.rdb) 移动到 redis 安装目录并启动服务即可
备份成功后故意用flushdb清空redis,看看是否可以恢复数据 (执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的,无意义)
物理恢复,一定服务和备份分机隔离
手动触发
Redis提供了两个命今来生成RDB文件分别是save和bgsave
save:在主程序中执行会阻塞当前redis服务器,直到持久化工作完成执行save命令期间,Redis不能处理其他命令,线上禁止使用
bgSave:Redis会在后台异步进行快照操作,不阻塞快照同时还可以响应客户端请求,该触发方式会fork一个子进程由子进程复制持久化过程
Redis会在后台异步进行快照操作,不阻塞快照同时还可以响应客户端请求,该触发方式会fork一个子进程由子进程复制持久化过程
LASTSAVE:可以通过lastsave命令获取最后一次成功执行快照的时间
优势
适合大规模的数据恢复
按照业务定时备份
对数据完整性和一致性要求不高
RDB文件在内存中的加载速度要比 AOF 快得多
劣势
在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,快照之间的数据会丢失
内存数据的全量同步,如果数据量太大会导致I/0严重影响服务器性能
RDB依赖工主讲程的fork,在更大的数据集中,这可能会导致服务请求的暖间延迟fork的时候内存中的数据被克隆了一份,大致2倍的膨胀性,需要考虑
如何检查修复dump.rdb文件
redis-check-rdb dump6379.rdb
那些情况会触发RDB快照
配置文件中默认的快照配置
手动save/bgsave命令
执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的,无意义
执行shutdown且没有设置开启AOF持久化
主从复制时,主节点自动触发
如何禁用快照
动态所有停止RDB保存规则的方法: redis-cli config set save ""
配置文件禁用
AOF(Append Only File)
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件旧不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
默认情况下,redis是没有开启AOF(append only file)的。开启AOF功能需要设置配置: appendonly yes
AOF持久化工作流程
1.Client作为命令的来源,会有多个源头以及源源不断的请求命令。
2.在这些命令到达Redis Server 以后并不是直接写入AOF文件,会将其这些命令先放入AOF缓存中进行保存。这里的AOF缓冲区实际上是内存中的一片区域,存在的目的是当这些命令达到一定量以后再写入磁盘,避免频繁的磁盘IO操作。
3.AOF缓冲会根据AOF缓冲区同步文件的三种写回策略将命令写入磁盘上的AOF文件。
4.随着写入AOF内容的增加为避免文件膨胀,会根据规则进行命令的合并(又称AOF重写),从而起到AOF文件压缩的目的。
5.当Redis Server 服务器重启的时候会从AOF文件载入数据。
三种写回策略
Always:同步写回,每人写命令执行完立刻同步地将日志写回磁盘
everysec:每秒写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔1秒把缓冲区中的内容写入磁盘
no:操作系统控制的写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
三种写回策略小总结update
配置文件说明:Redis7之前的aof文件只会保存一个,7之后会保存为三个,三个文件分别为:base基本文件;incr增量文件;manifest清单文件
正常恢复
启动:修改默认的appendonly no,改为yes;
修改aof文件保存路径 dir /myredis
异常恢复
故意乱写正常的AOF文件模拟网络闪断文件写error ,重启 Redis 之后就会进行 AOF 文件的载入,发现启动都不行,使用异常修复命令: redis-check-aof --fix 进行修复
优势
更好的保护数据不丢失 、性能高、可做紧急恢复
劣势
相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb
aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同
AOF重写机制:启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。
触发机制
自动触发:满足配置文件中的选项后,Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时
手动触发:客户端向服务器发送bgrewriteaof命令
开启重写机制:开启aof;关闭混合,设置为no
重写原理
1:在重写开始前,redis会创建一个“重写子进程”,这个子进程会读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
2:与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
3:当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中
4:当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中
5:重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似
关闭混合,设置为no
RDB+AOF混合持久化
RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件未尾。
数据恢复顺序和加载流程
同时开启两种持久化方式
在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?
作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),留着rdb作为一个万一的手段。
RDB+AOF混合方式
结合了RDB和AOF的优点,既能快速加载又能避免丢失过多的数据。
1.开启混合方式设置
设置aof-use-rdb-preamble的值为 yes yes表示开启,设置为no表示禁用
2.RDB+AOF的混合方式---------> 结论:RDB镜像做全量持久化,AOF做增量持久化
先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录。这样的话,重启服务的时候会从RDB和AOF两部分恢复数据,既保证了数据完整性,又提高了恢复数据的性能。简单来说:混合持久化方式产生的文件一部分是RDB格式,一部分是AOF格式。----》AOF包括了RDB头部+AOF混写
纯缓存模式:同时关闭RDB+AOP
Redis事务
可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞
一个队列中,一次性、顺序性、排他性的执行一系列命令
与MySQL数据库事务的区别
1.单独的隔离操作:Redis的事务仅仅是保证事务里的操作会被连续独占的执行,redis命令执行是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的
2.没有隔离级别的概念:因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题了
3.不保证原子性:Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力
4.排它性:Redis会保证一个事务内的命令依次执行,而不会被其它命令插入
Redis 事务命令:
DISCARD:取消事务,放弃执行事务块内的所有命令
EXEC:执行所有事务块内的命令
MULTI:标记一个事务块的开始.
UNWATCH:取消 WATCH 命令对所有 key 的监视
WATCH key [key ...]:监视一个(或多个)key如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
watch监控:Redis使用Watch来提供乐观锁定,类似于CAS(Check-and-Set)
悲观锁:悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
乐观锁:乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
乐观锁策略:提交版本必须 大于 记录当前版本才能执行更新
一旦执行了exec之前加的监控锁都会被取消掉了
当客户端连接丢失的时候(比如退出链接),所有东西都会被取消监视
小结:
开启: 以MULTI开始一个事务
入队: 将多个命令入队到事务中,接到这些命令并不会立即执行而是放到等待执行的事务队列里面
执行: 由EXEC命令触发事务
Redis管道(pipeline)
pipeline是为了解决RTT往返回时,仅仅是将命令打包一次性发送对整个Redis的执行不造成其它任何影响
批处理命令变种优化措施,类似Redis的原生批命令(mget和mset)
pipeline与原生批量命令对比:
原生批量命令是原子性(例如:mset,mget),pipeline是非原子性
原生批量命令一次只能执行一种命令,pipeline支持批量执行不同命令
原生批命令是服务端实现,而pipeline需要服务端与客户端共同完成
pipeline与事务对比:
事务具有原子性,管道不具有原子性
管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到exec命令后才会执行,管道不会
执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会
注意事项:
pipeline缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令
使用pipeline组装的命令个数不能大多,不然数据量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存
面试题:如何优化频繁命令往返造成的性能瓶颈?
Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。一个请求会遵循以下步骤:
1.客户端向服务端发送命令分四步(发送命令→命令排队→命令执行→返回结果),并监听Socket返回,通常以阻塞模式等待服务端响应。
2.服务端处理命令,并将结果返回给客户端。
上述两步称为:Round Trip Time(简称RTT,数据包往返于两端的时间)
如果同时需要执行大量的命令,那么就要等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip),而且还频繁调用系统IO,发送网络请求,同时需要redis调用多次read()和write()系统方法,系统方法会将数据从用户态转移到内核态,这样就会对进程上下文有比较大的影响了,性能不太好
Redis复制
就是主从复制,master以写为主,Slave以读为主
当master数据变化的时候,自动将新的数据异步同步到其它slave数据库
作用:读写分离;容灾恢复;数据备份;水平扩容支撑高并发
权限细节
master如果配置了requirepass参数,那么slave就要配置masterauth来设置校验密码,否则的话master会拒绝slave的访问请求
基础操作命令
info replication:可以查看复制节点的主从关系和配置信息
replicaof 主库IP 主库端口:一般写入进redis.conf配置文件内
slaveof 主库IP 主库端口:每次与master断开之后,都需要重新连接,除非你配置进redis.conf文件在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库那么会停止和原主数据库的同步关系转而和新的主数据库同步,重新拜码头
slaveof no one:使当前数据库停止与其他数据库的同步,转成主数据库,自立为王
配置
1.开启daemonize yes
2.注释掉bind 127.0.0.1
3.protected-mode no
4.指定端口 port
5.指定当前工作目录,dir /
6.pid文件名字,pidfile
7.log文件名字,logfile
8.requirepass
9.dump.rdb名字
10.aof文件,appendfilename
11.从机访问主机的通行密码masterauth,从机必须配置,主机不用
面试题:
从机可以执行写命令吗?不可以
slave是从头开始复制还是从切入点开始复制? 首次一锅端,后续跟随,master写,slave跟
主机shutdown后情况如何?从机是上位还是原地待命? 从机不动,原地待命,从机数据可以正常使用;等待主机重启动归来
主机shutdowr重启后,主从关系还在吗? 从机还能否顺利复制? 青山依旧在
某台从机down后,master继续,从机重启后它能跟上大部队吗? 可以
主仆:slaveof 主库IP 主库端口
薪火相传:slaveof 新主库IP 新主库端口
一个slave可以是下一个slave的master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master可以有效减轻主master的写压力
中途变更转向:会清除之前的数据,重新建立拷贝最新的
反客为主:SLAVEOF no one 使当前数据库停止与其他数据库的同步,转成主数据库
slave启动,同步初请:
slave启动成功连接到master后会发送一个sync命令
slave首次全新连接master.一次完全同步(全量复制)将被自动执行,slave自身原有数据会被master数据覆盖清除
首次连接,全量复制:
master节点收到sync命令后会开始在后台保存快照(即RDB持久化,主从复制时会触发RDB),同时收集所有接收到的用于修改数据集命令缓存起来,master节点执行RDB持久化完后,master将rdb快照文件和所有缓存的命令发送到所有slave,以完成一次完全同步
而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化
心跳持续,保持通信:epl-ping-replica-period 10 master发出PING包的周期,默认是10秒
进入平稳,增量复制:Master继续将新的所有收集到的修改命令自动依次传给slave,完成同步
从机下线,重连续传:
master会检查backlog里面的offset,master和slave都会保存一个复制的offseti还有一个masterId,offset是保存在backlog中的。Master只会把已经复制的offset后面的数据复制给Slave,类似断点续传
复制的缺点
复制延时,信号衰减:由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
master挂了如何办?默认情况下,不会在slave节点中自动重选master
Redis哨兵(sentinel)
吹哨人巡查监控后台master主机是否故障,如果故障了根据投票数自动将某一个从库转换为新主库,继续对外服务
哨兵的作用:1、监控redis运行状态,包括master和slave;2、当master down机,能自动将slave切换成新master
主从监控:监控主从redis库运行是否正常
消息通知:哨兵可以将故障转移的结果发送给客户端
故障转移:如果Master异常,则会进行主从切换将其中一个Slave作为新Master
配置中心:客户端通过连接哨兵来获得当前Redis服务的主节点地址
案例:
sentinel.conf配置文件
重要参数项说明:
bind:服务监听地址,用于客户端连接,默认本机地址
daemonize:是否以后台daemon方式运行
protected-mode:安全保护模式
port:端口
logfile:日志文件路径
pidfile:pid文件路径
dir:工作目录
sentinel monitor <master-name> <ip> <redis-port> <quorum>:设置要监控的master服务器,quorum表示最少有几个哨兵认可客观下线同意故障迁移的法定票数。
网络是不可靠的,有时候一个sentinel会因为网络堵塞而误以为一个master redis已经死掉了,在sentinel集群环境下需要多个sentinel互相沟通来确认某个master是否真的死了,quorum这个参数是进行客观下线的一个依据,意思是至少有quorum个sentinel认为这个master有故障,才会对这个master进行下线以及故障转移。因为有的时候,某个sentinel节点可能因为自身网络原因,导致无法连接master,而此时master并没有出现故障,所以,这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,这就保证了公平性和高可用。
sentinel.conf文件内容 bind 0.0.0.0 daemonize yes protected-mode no port 26379 logfile "/myredis/sentinel26379.log" pidfile /var/run/redis-sentinel26379.pid dir /myredis sentinel monitor mymaster 256.256.256.256 6379 2 sentinel auth-pass mymaster root
sentinel auth-pass <master-name> <password>:master设置了密码,连接master服务的密码
启动哨兵命令:redis-sentinel sentinel26379.conf
配置文件对比:文件的内容,在运行期间会被sentinel动态进行更改
Master-Slave切换后,master redis.conf、 slave_redis.conf和sentinel.conf的内容都会发生改变,即master redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换
哨兵运行流程和选举原理:
当一个主从配置中的master失效之后,sentinel可以选举出一个新的master,用于自动接替原master的工作,主从配置中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。
运行流程,故障切换:
SDOWN主观下线:
SDOWN(主观不可用)是单个sentinel自己主观上检测到的关于master的状态,从sentinel的角度来看,如果发送了PING心跳后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件。
sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度。
所谓主观下线(Subjectively Down, 简称 SDOWN)指的是单个Sentinel实例对服务器做出的下线判断,即单个sentinel认为某个服务下线(有可能是接收不到订阅,之间的网络不通等等原因)。主观下线就是说如果服务器在[sentinel down-after-milliseconds]给定的毫秒数之内没有回应PING命令或者返回一个错误消息, 那么这个Sentinel会主观的(单方面的)认为这个master不可以用了,
sentinel down-after-milliseconds <masterName> <timeout>
表示master被当前sentinel实例认定为失效的间隔时间,这个配置其实就是进行主观下线的一个依据
master在多长时间内一直没有给Sentine返回有效信息,则认定该master主观下线。也就是说如果多久没联系上redis-servevr,认为这个redis-server进入到失效(SDOWN)状态。
ODOWN客观下线:
ODOWN需要一定数量的sentinel,多个哨兵达成一致意见才能认为一个master客观上已经宕掉
sentinel monitor <master- name> <ip> <redis- port><quorum>
四个参数含义:
masterName是对某个master+slave组合的一个区分标识(一套sentinel可以监听多组master+slave这样的组合)
quorum这个参数是进行客观下线的一个依据,法定人数/法定票数
意思是至少有quorum个sentinel认为这个master有故障才会对这个master进行下线以及故障转移。因为有的时候,某个sentinel节点可能因为自身网络原因导致无法连接master,而此时master并没有出现故障,所以这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,这就保证了公平性和高可用。
选举出领导者哨兵:
当主节点被判断客观下线以后,各个哨兵节点会进行协商先选举出一个领导者哨兵节点 (兵王)并由该领导者节点也即被选举出的兵王进行failover (故障迁移)。
如何选举?Raft算法。
监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得:
即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者
新主登基:
选出新master的规则,剩余slave节点健康前提下
redis.conf文件中,优先级slave-priority或者replica-priority最高的从节点(数字越小优先级越高
复制偏移位置offset最大的从节点
最小Run ID的从节点,字典顺序,ASCII码
群臣俯首:
执行slaveof no one命令让选出来的从节点成为新的主节点,并通过slaveof命令让其他节点成为其从节点
Sentinel leader会对选举出的新master执行slaveof no one操作,将其提升为master节点
Sentinelleader向其它slave发送命令,让剩余的slave成为新的master节点的slave
旧主拜服:
将之前已下线的老master设置为新选出的新master的从节点,当老master重新上线后,它会成为新master的从节点
Sentinelleader会i-原来的master降级为slave并恢复正常工作
哨兵使用注意事项:
哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用;
哨兵节点的数量应该是奇数;
各个哨兵节点的配置应一致;
如果哨兵节点部署在Docker等容器里面,尤其要注意端口的正确映射;
哨兵集群+主从复制,并不能保证数据零丢失;
Redis集群(cluster)
由于数据量过大,单个Master复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展每个复制集只负责存储整个数据集
的一部分,这就是Redis的集群,其作用是提供在多个Redis节点间共享数据的程序集。
Redis集群是一个提供在多个Redis节点间共享数据的程序集
作用:
Redis集群可以支持多个Master
Redis集群支持多个Master,每个Master又可以挂载多个slave:读写分离;支持数据的高可用;支持海量数据的读写存储操作
由于Cluster自带Sentinel的故障转移机制,内置了高可用的支持,无需再去使用哨兵功能
客户端与Redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可
槽位slot负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系
集群算法-分片-槽位slot
分片是什么:使用Redis集群时我们会将存储的数据分散到多台redis机器上,这称为分片。简言之,集群中的每个Redis实例都被认为是整个数据的一个分片。
如何找到给定key的分片:为了找到给定key的分片,我们对key进行CRC16(key)算法处理并通过对总分片数量取模。然后,使用确定性哈希函数,这意味着给定的key将多次始终映射到同一个分片,我们可以推断将来读取特定key的位置。
优势:方便扩缩容和数据分派查找
slot槽位映射,一般业界有3种解决方案
哈希取余分区
2亿条记录就是2亿个k,v,我们单机不行必须要分布式多机,假设有3台机器构成一个集群,用户每次读写操作都是根据公式:
hash(key) % N个机器台数,计算出哈希值,用来决定数据映射到哪一个节点上。
优点:简单粗暴,直接有效,只需要预估好数据规划好节点,例如3台、8台、10台,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡+分而治之的作用。
缺点:原来规划好的节点,进行扩容或者缩容就比较麻烦了额,不管扩缩,每次数据变动导致节点有变动,映射关系需要重新进行计算,在服务器个数固定不变时没有问题,如果需要弹性扩容或故障停机的情况下,原来的取模公式就会发生变化:Hash(key)/3会变成Hash(key) /?。此时地址经过取余运算的结果将发生很大变化,根据公式获取的服务器也会变得不可控。
某个redis机器宕机了,由于台数数量变化,会导致hash取余全部数据重新洗牌。
一致性哈希算法分区
一致性哈希算法在1997年由麻省理工学院中提出的,设计目标是为了解决:分布式缓存数据变动和映射问题,某个机器宕机了,分母数量改变了,自然取余数不OK了。
提出一致性Hash解决方案目的是当服务器个数发生变动时尽量减少影响客户端到服务器的映射关系
3大步骤
算法构建一致性哈希环
一致性哈希算法必然有个hash函数并按照算法产生hash值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个hash空间[0,2^32-1],这个是一个线性空间,但是在算法中,我们通过适当的逻辑控制将它首尾相连(0 = 2^32),这样让它逻辑上形成了一个环形空间。
它也是按照使用取模的方法,前面笔记介绍的节点取模法是对节点(服务器)的数量进行取模。而一致性Hash算法是对2^32取模,简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希环如下图:整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1, 0和2^32-1在零点中方向重合,我们把这个由2^32个点组成的圆环称为Hash环。
redis服务器IP节点映射
节点映射:将集群中各个IP节点映射到环上的某一个位置。
将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。假如4个节点NodeA、B、C、D,经过IP地址的哈希函数计算(hash(ip)),使用IP地址哈希后在环空间的位置如下:
key落到服务器的落键规则
当我们需要存储一个kv键值对时,首先计算key的hash值,hash(key),将这个key使用相同的函数Hash计算出哈希值并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。
如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:根据一致性Hash算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。
优点:一致性哈希算法的容错性:假设Node C宕机,可以看到此时对象A、B、D不会受到影响。一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。简单说,就是C挂了,受到影响的只是B、C之间的数据且这些数据会转移到D进行存储。
一致性哈希算法的扩展性:数据量增加了,需要增加一台节点NodeX,X的位置在A和B之间,那收到影响的也就是A到X之间的数据,重新把A到X的数据录入到X上即可,不会导致hash取余全部数据重新洗牌。
缺点:一致性哈希算法的数据倾斜问题:一致性Hash算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。
总结:为了在节点数目发生改变时尽可能少的迁移数据,将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到临近的存储节点存放。而当有节点加入或退出时仅影响该节点在Hash环上顺时针相邻的后续节点。
优点:加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。
缺点:数据的分布和节点的位置有关,因为这些节点不是均匀的分布在哈希环上的,所以数据在进行存储时达不到均匀分布的效果。
哈希槽分区:HASH_SLOT = CRC16(key) mod 16384
哈希槽实质就是一个数组,数组[0,2^14 -1]形成hash slot空间。
解决均匀分配的问题,在数据和节点之间又加入了一层,把这层称为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据分配
一个集群只能有16384个槽,编号0-16383(0-2^14-1)。这些槽会分配给集群中的所有主节点,分配策略没有要求。
集群会记录节点和槽的对应关系,解决了节点和槽的关系后,接下来就需要对key求哈希值,然后对16384取模,余数是几key就落入对应的槽里。HASH_SLOT = CRC16(key) mod 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
哈希槽计算:Redis 集群中内置了 16384 个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在 Redis 集群中放置一个 key-value时,redis先对key使用crc16算法算出一个结果然后用结果对16384求余数[ CRC16(key) % 16384],这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,也就是映射到某个节点上。
面试题:为什么redis集群的最大槽数是16384个?
Redis集群并没有使用一致性hash而是引入了哈希槽的概念。Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。但为什么哈希槽的数量是16384(2^14)个呢?
CRC16算法产生的hash值有16bit,该算法可以产生2^16=65536个值。
换句话说值是分布在0~65535之间,有更大的65536不用为什么只用16384就够?
作者在做mod运算的时候,为什么不mod65536,而选择mod16384? HASH_SLOT = CRC16(key) mod 65536为什么没启用?(https://github.com/redis/redis/issues/2576)
原因是:正常的心跳数据包携带节点的完整配置,可以以幂等的方式用旧的替换,以更新旧的配置。这意味着它们包含原始形式的节点的插槽配置,该节点使用2k的空间和16k的插槽,但使用65k的插槽将使用令人望而却步的8k的空间。
同时,由于其他设计权衡,Redis集群不太可能扩展到1000多个主节点。因此,16k在正确的范围内,可以确保每个主机有足够的插槽,最多1000个材料,但数量足够小,可以很容易地将插槽配置作为原始位图传播。注意,在小簇中,位图将很难压缩,因为当N很小时,位图将设置槽/N位,这是设置的位的很大百分比。
(1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时,这块的大小是: 16384÷8÷1024=2kb
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
(2)redis的集群主节点数量基本不可能超过1000个。
集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
(3)槽位越小,节点少的情况下,压缩比高,容易传输
Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
Redis集群不保证强一致性,这意味着在特定的条件下,Redis集群可能会丢掉一些被系统收到的写入请求命令
案例:
redisCluster6381.conf
启动Redis实例:redis-server /myredis/cluster/redisCluster6381.conf
通过redis-cli命令为6台机器构建集群关系
构建主从关系命令:redis-cli -a root --cluster create --cluster-replicas 1 256.256.256.256:6381 ...
--cluster-replicas 1 表示为每个master创建一个slave节点
验证集群状态命令:info replication;cluster info;cluster nodes
进入Redis: redis-cli -a root -p 6381 -c 加入参数-c,优化路由
手动故障转移 or 节点从属调整该如何处理:常用命令:CLUSTER FAILOVER
集群常用操作命令和CRC16算法分析
不在同一个slot槽位下的多键操作支持不好,通识占位符登场:不在同一个slot槽位下的键值无法使用mset、mget等多键操作
可以通过{}来定义同一个组的概念,使key中{}内相同内容的键值对放到一个slot槽位去,mget k1{x} k2{x} k3{x}
Redis集群有16384个哈希槽,每key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽
常用命令:集群是否完整才能对外提供服务:cluster-require-full-coverage
默认YES,现在集群架构是3主3从的redis cluster由3个master平分16384个slot,每个master的小集群负责1/3的slot,对应一部分数据。
cluster-require-full-coverage: 默认值 yes , 即需要集群完整性,方可对外提供服务 通常情况,如果这3个小集群中,任何一个(1主1从)挂了,你这个集群对外可提供的数据只有2/3了, 整个集群是不完整的, redis 默认在这种情况下,是不会对外提供服务的。
如果你的诉求是,集群不完整的话也需要对外提供服务,需要将该参数设置为no ,这样的话你挂了的那个小集群是不行了,但是其他的小集群仍然可以对外提供服务。
CLUSTER COUNTKEYSINSLOT 槽位数字编号:1,该槽位被占用;0,该槽位没占用
CLUSTER KEYSLOT 键名称:该键应该存在哪个槽位上
BigKey
面试题:
阿里广告平台,海量数据里查询某一固定前缀的key
小红书,你如何生产上限制keys */flushdb/flushall等危险命令以防止误删误用?
美团,MEMORY USAGE 命令你用过吗?
BigKey问题,多大算big? 你如何发现? 如何删除? 如何处理?
BigKey你做过调优吗? 惰性释放lazyfree了解过吗?
Morekey问题,生产上redis数据库有1000W记录,你如何遍历? key *可以吗?
MoreKey:
大批量往redis里面插入2000W测试数据key
生成100W条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中
for((i=1;i<=100*10000;i++)); do echo "set k$i v$i" >> /tmp/redisTest.txt ;done;
通过redis提供的管道--pipe命令插入100W大批量数据
cat /tmp/redisTest.txt | redis-cli -h 127.0.0.1 -p 6379 -a root --pipe
生产上限制keys */flushdb/flushall等危险命令以防止误删误用?
通过配置设置禁用这些命令,redis.conf在SECURITY这一项中
scan命令
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
SCAN 返回一个包含两个元素的数组: 第一个元素是用于进行下一次迭代的新游标;第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回零表示迭代已结束。
SCAN的遍历顺序:它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
BigKey:
多大算big:string是value,最大512MB但是>10KB就是bigkey;list、hash、set和zset,个数超过5000就是bigkey
危害:内存不均,集群迁移困难;超时删除,大key删除作梗;网络流量阻塞
如何产生:社交类;汇总统计
如何发现
redis-cli --bigkeys(redis-cli --bigkeys -a root)
好处:给出每种数据结构Top 1 bigkey,同时给出每种数据类型的键值个数+平均大小
不足:想查询大于10kb的所有key,--bigkeys参数就无能为力了,需要用到memory usage来计算每个键值的字节数
MEMORY USAGE 键:计算每个键值的字节数
如何删除:
String:一般用del,如果过于庞大unlink
hash:使用hscan每次获取少量field-value,再使用hdel删除每个field
list:使用ltrim渐进式逐步删除,直到全部删除完成
set:使用sscan每次获取部分元素,再使用srem命令删除每个元素
zset:使用zscan每次获取部分元素,再使用ZREMRANGEBYRANK命令删除每个元素
生产调优:redis.conf配置文件LAZY FREEING相关说明
阻塞和非阻塞删除命令
Redis 有两个原语来删除键。一种称为 DEL,是对象的阻塞删除这意味着服务器停止处理新命令,以便以同步方式回收与对象关联的所有内存。如果删除的键与一个小对象相关联,则执行 DEL 命令所需的时间非常短,可与大多数其他命令相媲美Redis 中的 O(1)或 o(log_N) 命令。 但是,如果键与包含数百万个元素的聚合值相关联,则服务器可能会阻塞很长时间 (甚至几秒钟) 才能完成操作。
基于上述原因,Redis 还提供了非阻塞删除原语,例如 UNLINK (非阻塞 DEL) 以及 FLUSHALL 和 FLUSHDB 命令的 ASYNC 选项,以便在后台回收内存。这些命令在恒定时间内执行。另一个线程将尽可能快地逐步释放后台中的对象。
FLUSHALL和 FLUSHDB 的 DEL、UNLINK 和 ASYNC 选项是用户控制的。这取决于应用程序的设计,以了解何时使用其中一个是个好主意。然而,作为其他操作的副作用,Redis 服务器有时不得不删除键或刷新整数据库。
优化配置
缓存双写一致性之更新策略探讨
面试题:
你只要用缓存,就可能会涉及到redis缓存与数据库双存储双写你只要是双写,就一定会有数据一致性的问题那么你如何解决一致性问题?
双写一致性,你先动缓存redis还是数据库mysgl哪一个? why?
延时双删你做过吗? 会有哪些问题?
有这么一种情况,微服务查询redis无mysql有,为保证数据双写一致性回写redis你需要注意什么?
双检加锁策略你了解过吗? 如何尽量避免缓存击穿?
redis和mysql双写100%会出纰漏,做不到强一致性,你如何保证最终一致性?
缓存双写一致性:
如果redis中有数据:需要和数据库中的值相同
如果redis中无数据:数据库中的值要是最新值,且准备回写redis
缓存按照操作可分为只读缓存和读写缓存
读写缓存:同步直写策略;异步缓写策略
同步直写策略
写数据库后也同步写redis缓存,缓存和数据库中的数据一致;对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略
异步缓写策略
正堂业务运行中,mysal数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统;异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写
双检加锁策略:多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
public string get(string key) { String value = redis.get(key); //查询缓存 if (value != null) {//缓存存在直接返回 return value; } else { //缓存不存在则对方法加锁 //假设请求量很大,缓存过期 synchronized (TestFuture.class) { value = redis.get(key);//再查一遍redis if (value != null) {//查到数据直接返回 return value; } else { //二次查询缓存也不存在,直接查DB value = dao.get(key);//数据缓存 redis.setnx(key, value, time);//返回 return value; } } } }
数据库和缓存一致性的几种更新策略:先更新数据库,再更新缓存;先更新缓存,再更新数据库;先删除缓存,再更新数据库;先更新数据库,再删除缓存
如何选择方案?利弊如何?
优先使用先更新数据库,再删除缓存的方案(先更库→后删存)。理由如下:
1 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。
2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
多补充一句:如果使用先更新数据库,再删除缓存的方案
如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性。
canal(https://github.com/alibaba/canal/)
canal [kə'næl],中文翻译为 水道/管道/沟渠/运河,主要用途是用于 MySQL 数据库增量日志数据的订阅、消费和解析,是阿里巴巴开发并开源的,采用Java语言开发;历史背景是早期阿里巴巴因为杭州和美国双机房部署,存在跨机房数据同步的业务需求,实现方式主要是基于业务 trigger(触发器) 获取增量变更。从2010年开始,阿里巴巴逐步尝试采用解析数据库日志获取增量变更进行同步,由此衍生出了canal项目;
作用:数据库镜像;数据库实时备份;索引构建和实时维护(拆分异构索引、倒排索引等);业务 cache 刷新;带业务逻辑的增量数据处理
传统MySQL主从复制工作原理
MySQL的主从复制将经过如下步骤:
1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中;
2、salve 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测,探测其是否发生过改变,如果探测到 master 主服务器的二进制事件日志发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志;
3、同时 master 主服务器为每个 I/O Thread 启动一个dump Thread,用于向其发送二进制事件日志;
4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中;
5、salve 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致;
6、最后 I/O Thread 和 SQL Thread 将进入睡眠状态,等待下一次被唤醒;
canal工作原理
canal 模拟 MysQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送 dump 协议
MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal)
canal 解析 binary log 对象(原始为 byte 流)
mysql-canal-redis双写一致性Coding
MYSQL
查看mysql版本:SELECT VERSION0;
当前的主机二进制日志:show master status;
查看SHOW VARIABLES LIKE 'log_bin';
my.imi:
log-bin=mysql-bin #开启 binlog
binlog-format=ROW #选择 ROW 模式
server_id=1 #配置MySQL replaction需要定义,不要和canal的 slaveId重复
ROW模式 除了记录sql语句之外,还会记录每个字段的变化情况,能够清楚的记录每行数据的变化历史,但会占用较多的空间。
STATEMENT模式只记录了sql语句,但是没有记录上下文信息,在进行数据恢复的时候可能会导致数据的丢失情况;
MIX模式比较灵活的记录,理论上说当遇到了表结构变更的时候,就会记录为statement模式。当遇到了数据更新或者删除情况下就会变为row模式;
重启MySQL,再次查看SHOW VARIABLES LIKE 'log_bin';
授权canal连接MySQL账号:
DROP USER IF EXISTS 'canal'@'%';
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal'; (5)
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' WITH GRANT OPTION; (8)
FLUSH PRIVILEGES;
SELECT * FROM mysql.user;
canal(https://github.com/alibaba/canal/releases/tag/canal-1.1.6)
修改/mycanal/conf/example路径下instance.properties文件
canal.instance.master.address 换成自己的mysql主机master的IP地址
canal.instance.dbUsername 换成自己的在mysgl新建的canal账户
查看 server 日志、 样例example 的日志 判新canal是否启动成功
import com.alibaba.fastjson.JSONObject; import com.alibaba.otter.canal.client.CanalConnector; import com.alibaba.otter.canal.client.CanalConnectors; import com.alibaba.otter.canal.protocol.CanalEntry.*; import com.alibaba.otter.canal.protocol.Message; import com.atguigu.utils.RedisUtils; import redis.clients.jedis.Jedis; import java.net.InetSocketAddress; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; public class RedisCanalClientExample { public static final Integer _60SECONDS = 60; public static final String REDIS_IP_ADDR = "192.168.126.100"; private static void redisInsert(List<Column> columns){ JSONObject jsonObject = new JSONObject(); for (Column column : columns) { System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated()); jsonObject.put(column.getName(),column.getValue()); } if(columns.size() > 0) { try(Jedis jedis = RedisUtils.getJedis()) { jedis.set(columns.get(0).getValue(),jsonObject.toJSONString()); }catch (Exception e){ e.printStackTrace(); } } } private static void redisDelete(List<Column> columns){ JSONObject jsonObject = new JSONObject(); for (Column column : columns) { jsonObject.put(column.getName(),column.getValue()); } if(columns.size() > 0) { try(Jedis jedis = RedisUtils.getJedis()) { jedis.del(columns.get(0).getValue()); }catch (Exception e){ e.printStackTrace(); } } } private static void redisUpdate(List<Column> columns){ JSONObject jsonObject = new JSONObject(); for (Column column : columns) { System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated()); jsonObject.put(column.getName(),column.getValue()); } if(columns.size() > 0) { try(Jedis jedis = RedisUtils.getJedis()) { jedis.set(columns.get(0).getValue(),jsonObject.toJSONString()); System.out.println("---------update after: "+jedis.get(columns.get(0).getValue())); }catch (Exception e){ e.printStackTrace(); } } } public static void printEntry(List<Entry> entrys) { for (Entry entry : entrys) { if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) { continue; } RowChange rowChage = null; try { //获取变更的row数据 rowChage = RowChange.parseFrom(entry.getStoreValue()); } catch (Exception e) { throw new RuntimeException("ERROR ## parser of eromanga-event has an error,data:" + entry.toString(),e); } //获取变动类型 EventType eventType = rowChage.getEventType(); System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s", entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType)); for (RowData rowData : rowChage.getRowDatasList()) { if (eventType == EventType.INSERT) { redisInsert(rowData.getAfterColumnsList()); } else if (eventType == EventType.DELETE) { redisDelete(rowData.getBeforeColumnsList()); } else {//EventType.UPDATE redisUpdate(rowData.getAfterColumnsList()); } } } } public static void main(String[] args){ System.out.println("---------O(∩_∩)O哈哈~ initCanal() main方法-----------"); //================================= // 创建链接canal服务端 CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(REDIS_IP_ADDR,11111), "example", "", ""); int batchSize = 1000; //空闲空转计数器 int emptyCount = 0; System.out.println("---------------------canal init OK,开始监听mysql变化------"); try { connector.connect(); //connector.subscribe(".*\\..*"); connector.subscribe("cloud_test.t_user"); connector.rollback(); int totalEmptyCount = 10 * _60SECONDS; while (emptyCount < totalEmptyCount) { System.out.println("我是canal,每秒一次正在监听:"+ UUID.randomUUID().toString()); Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据 long batchId = message.getId(); int size = message.getEntries().size(); if (batchId == -1 || size == 0) { emptyCount++; try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } else { //计数器重新置零 emptyCount = 0; printEntry(message.getEntries()); } connector.ack(batchId); // 提交确认 // connector.rollback(batchId); // 处理失败, 回滚数据 } System.out.println("已经监听了"+totalEmptyCount+"秒,无任何消息,请重启重试......"); } finally { connector.disconnect(); } } } public class RedisUtils { public static final String REDIS_IP_ADDR = "192.168.126.100"; public static final String REDIS_pwd = "root"; public static JedisPool jedisPool; static { JedisPoolConfig jedisPoolConfig=new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPool=new JedisPool(jedisPoolConfig,REDIS_IP_ADDR,6379,10000,REDIS_pwd); } public static Jedis getJedis() throws Exception { if(null!=jedisPool){ return jedisPool.getResource(); } throw new Exception("Jedispool is not ok"); } } server.port=5555 # ========================alibaba.druid===================== spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/cloud_test?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.username=root spring.datasource.password=123456 spring.datasource.druid.test-while-idle=false
bitmap/hyperloglog/GEO
面试题:
抖音电商直播,主播介绍的商品有评论,1个商品对应了1系列的评论,排序+展现+取前10条记录
用户在手机App上的签到打卡信息: 1天对应1系列用户的签到记录,新浪微博、钉钉打卡签到,来没来如何统计?
应用网站上的网页访问信息:1个网页对应1系列的访问点击,淘宝网首页,每天有多少人浏览首页?
你们公司系统上线后,说-下UV、PV、DAU分别是多少?
记录对集合中的数据进行统计:在移动应用中,需要统计每天的新增用户数和第2天的留存用户数;在电商网站的商品评论中,需要统计评论列表中的最新评论;在签到打卡中,需要统计一个月内连续打卡的用户数;在网页访问记录中,需要统计独立访客(Unique Visitor,UV)量。
需求痛点:亿级数据的收集+清洗+统计+展现;目的:存的进+取得快+多维度;
统计的类型有哪些?亿级系统中常见的四种统计:聚合统计;排序统计;二值统计;基数统计
聚合统计:统计多个集合元素的聚合结果,就是前面讲解过的交差并等集合统计;交并差集和聚合函数的应用
排序统计:抖音短视频最新评论留言的场景,请你设计一个展现列表。考察你的数据结构和设计思路
设计案例和回答思路:在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议使用ZSet
二值统计(bitmap):集合元素的取值就只有0和1两种;在钉钉上班签到打卡的场景中,我们只用记录有签到(1)或没签到(0)
基数统计(hyperloglog):指统计一个集合中不重复的元素个数
hyperloglog
什么是UV:Unique Visitor,独立访客,一般理解为客户端IP,需要去重考虑
什么是PV:Page View,页面浏览量,不用去重
什么是DAU:Daily Active User,日活跃用户量,登录或者使用了某个产品的用户数(去重复登录的用户)D常用于反映网站、互联网应用或者网络游戏的运营情况
什么是MAU:Monthly Active User,月活跃用户量
GEO(http://api.map.baidu.com/lbsapi/getpoint/)
面试题说明:
移动互联网时代LBS应用越来越多,交友软件中附近的小姐姐、外卖软件中附近的美食店铺、打车软件附近的车辆等等。
那这种附近各种形形色色的XXX地址位置选择是如何实现的?
会有什么问题呢?
1.查询性能问题,如果并发高,数据量大这种查询是要搞垮mysql数据库的
2.一般mysql查询的是一个平面矩形访问,而叫车服务要以我为中心N公里为半径的圆形覆盖。
3.精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差,mysql不合适
bitmap(由0和1状态表现的二进制位的bit数组)
面试题说明:
日活统计
连续签到打卡
最近一周的活跃用户
统计指定用户一年之中的登陆天数
某用户按照一年365天,哪几天登陆过? 哪几天没有登陆? 全年中登录的天数共计多少?
说明:用String类型作为底层数据结构实现的一种统计二值状态的数据类型
位图本质是数组,它是基于String数据类型的按位的操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们可以称之为一个索引或者位格)。Bitmap支持的最大位数是2^32位,它可以极大的节约存储空间,使用512M内存就可以存储多大42.9亿的字节信息(2^32 = 4294967296)
布隆过滤器BloomFilter
面试题:
现有50亿个电话号码,现有10万个电话号码如何要快速准确的判断这些电话号码是否已经存在?
判断是否存在,布隆过滤器了解过吗?
安全连接网址,全球数10亿的网址判断
黑名单校验,识别垃圾邮件
白名单校验,识别出合法用户进行后续处理
目的:减少内存占用
方式:不保存数据信息,只是在内存中做一个是否存在的标记flag
布隆过滤器:由一个初值都为零的bit数组和多个哈希函数构成用来快速判断集合中是否存在某个元素
它实际上是一个很长的二进制数组(00000000)+一系列随机hash算法映射函数,主要用于判断一个元素是否在集合中。
通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。
链表、树、哈希表等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O(n),O(logn),O(1)。这个时候,布隆过滤器(Bloom Filter)就应运而生
高效地插入和查询,占用空间少,返回的结果是不确定性+不够完美。
一个元素如果判断结果:存在时,元素不-一定存在但是判断结果为不存在时,则一定不存在
布隆过滤器可以添加元素,但是不能删除元素由于涉及hashcode判断依据,删掉元素会导致误判率增加。
布隆过滤器原理:布隆过滤器(Bloom Filter) 是一种专门用来解决去重问题的高级数据结构。
实质就是一个大型位数组和几个不同的无偏hash函数(无偏表示分布均匀)。由一个初值都为零的bit数组和多个个哈希函数构成,用来快速判断某个数据是否存在。但是跟 HyperLogLog 一样,它也一样有那么一点点不精确,也存在一定的误判概率
添加key时:使用多个hash函数对key进行hash运算得到一个整数索引值,对位数组长度进行取模运算得到一个位置,每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了add操作。
查询key时:只要有其中一位是零就表示这个key不存在,但如果都是1,则不一定存在对应的key。
hash冲突导致数据不精准:
当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点,把它们置为 1(假定有两个变量都通过 3 个映射函数)。
查询某个变量的时候我们只要看看这些点是不是都是 1,就可以大概率知道集合中有没有它了如果这些点,有任何一个为零则被查询变量一定不在,如果都是 1,则被查询变量很可能存在,为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。
哈希函数的概念是:将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值。
如果两个散列值是不相同的(根据同一函数)那么这两个散列值的原始输入也是不相同的。
这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。
散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,这种情况称为“散列碰撞(collision)”。
用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞。
使用布隆过滤器时最好不要让实际元素数量远大于初始化数量,一次给够避免扩容
当实际元素数量超过初始化数量时,应该对布隆过滤器进行重建重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进行
优点:高效地插入和查询,内存占用bit空间少
缺点:不能删除元素。因为删掉元素会导致误判率增加,因为hash冲突同一个位置可能存的东西是多个共有的你删除一个元素的同时可能也把其它的删除了。
存在误判,不能精准过滤;有,是很可能有;无,是肯定无,100%无。
Redis的缓存过期淘汰机制
面试题:
生产上你们的redis内存设置多少?
如何配置、修改redis的内存大小
如果内存满了你怎么办
redis清理内存的方式?定期删除和惰性删除了解过吗
redis缓存淘汰策略有哪些?分别是什么?你用那个?
redis的LRU了解过吗? 请手写LRU
Iru和lfu算法的区别是什么?
redis默认内存多少?在哪里查看?如何设置修改?config get maxmemory;
查看Redis最大占用内存redis默认内存多少可以用?config get maxmemory
一般生产上你如何配置?一般配置Redis内存为最大物理内存的四分之三
如何修改redis内存设置?config set maxmemory;配置文件修改;
什么命令查看redis内存使用情况?info memory;config get maxmemory
真要打满了会怎么样? 如果Redis内存使用超出了设置的最大值会怎样? 报oom错误;
redis过期键是如何删除的?
三种不同的删除策略:立即删除;惰性删除;定期删除;
立即删除:(拿CPU运行时间换空间)
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。
这会产生大量的性能消耗,同时也会影响数据的读取操作。
惰性删除:(对memory不友好,用存储空间换区处理器性能;开启惰性淘汰:lazyfree-lazy-eviction=yes)
数据到达过期时间,不做处理。等下次访问该数据时如果未过期,返回数据;发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的。如果一个键已经过期,而这个键又仍然保留在redis中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏,无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息。
定期删除:
定期删除策略是前两种策略的折中:定期删除策略每隔一段时间执行一次删除过期键操作并通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理总结:周期性抽查存储空间(随机抽查,重点抽查)
定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行得太频繁或者执行的时间太长,定期删除策略就会退化成立即删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。
redis缓存淘汰策略(建议开启惰性淘汰:lazyfree-lazy-eviction=yes)
八种淘汰策略
1.noeviction:不会驱逐任何key,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都会返回error
2 allkeys-lru:对所有key使用LRU算法进行删除,优先删除掉最近最不经常使用的key,用以保存新数据
3.volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
4.allkeys-random:对所有key随机删除
5.volatile-random:对所有设置了过期时间的key随机删除
6.volatile-ttl:删除马上要过期的key
7.allkeys-lfu:对所有key使用LFU算法进行删除
8. volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
lru和lfu的区别:
LRU:最近最少使用页面置换算法,淘汰最长时间未被使用的页面,看页面最后一次被使用到发生调度的时间长短,首先淘汰最长时间未被使用的页面。
LFU:最近最不常用页面置换算法,淘汰一定时期内被访问次数最少的页,看一定时间段内页面被使用的频率,淘汰一定时期内被访问次数最少的页面。
举个栗子:某次时期Time为10分钟,如果每分钟进行一次调页,主存块为3,若所需页面走向为2121234假设到页面4时会发生缺页中断若按LRU算法,应换页面1(1页面最久未被使用),但按LFU算法应换页面3(十分钟内,页面3只使用了一次)可见LRU关键是看页面最后一次被使用到发生调度的时间长短,而LFU关键是看一定时间段内页面被使用的频率!
你平时用哪一种策略:
在所有的key都是最近最经常使用,那么就需要选择 alkeys-Iru进行置换最近最不经常使用的key,如果你不确定使用哪种策略,那么推荐使用allkeys-lru
如果所有的key的访问概率都是差不多的,那么可以选用allkeys-random 策略去置换数据
如果对数据有足够的了解,能够为key 指定hint(通过expire/ttl指定),那么可以选择volatile-ttl进行置换