redis
Redis
Redis简介
NoSQL简介
目前市场主流数据存储都是使用关系型数据库。每次操作关系型数据库时都是I/O操作,I/O操作是主要影响程序执行性能原因之一,连接数据库关闭数据库都是消耗性能的过程。尽量减少对数据库的操作,能够明显的提升程序运行效率。
针对上面的问题,市场上就出现了各种NoSQL(Not Only SQL,不仅仅可以使用关系型数据库)数据库,它们的宣传口号:不是什么样的场景都必须使用关系型数据库,一些特定的场景使用NoSQL数据库更好。
常见NoSQL数据库:
memcached :键值对,内存型数据库,所有数据都在内存中。
Redis:和Memcached类似,还具备持久化能力。
HBase:以列作为存储。
MongoDB:以Document做存储。
2.Redis简介
Redis是以Key-Value形式进行存储的NoSQL数据库。
Redis是使用C语言进行编写的。
平时操作的数据都在内存中,效率特高,读的效率110000/s,写81000/s,所以多把Redis当做缓存工具使用。
Redis以solt(槽)作为数据存储单元,每个槽中可以存储N多个键值对。Redis中固定具有16384。理论上可以实现一个槽是一个Redis。每个向Redis存储数据的key都会进行crc16算法得出一个值后对16384取余就是这个key存放的solt位置。
同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
主流功能和应用
1、分担MySQL压力,MySQL是存入硬盘,Redis是在内存中使用,日常使用中80%查询,20%写入。
2、分担流程:查找的时候先查找redis,找到直接返回,如果没有就去mysql,mysql返回数据,然后就把mysql写回redis,然后以后找redis就能找到了。
3、比如计数器,排行榜等方面就明显用redis优于mysql
4、内存存储和持久化 RDB和AOF,如果断电之后redis支持异步将内存中的数据写到硬盘上,然后有电就恢复到redis,不麻烦Mysql
5、高可用结构,单机,主从,哨兵,集群。为了避免redis挂了,服务压力过大。
6、缓存穿透,击穿,雪崩
7、分布式锁,跨服务器加锁就是用分布式锁。之前java的锁是针对JVM的,
8、队列list和set结构,消息队列平台,验证码发布
9、….
Redis单机版安装
1.安装依赖C语言依赖
redis使用C语言编写,所以需要安装C语言库
1 | # yum install -y gcc-c++ automake autoconf libtool make tcl |
2.上传并解压
把redis-5.0.5.tar.gz上传到/usr/local/tmp中
解压文件
1 | # cd /usr/local/tmp |
3.编译并安装
进入解压文件夹
1 | # cd /usr/local/tmp/redis-5.0.5/ |
编译
1 | # make |
安装
1 | # make install PREFIX=/usr/local/redis |
4.开启守护进程
复制cd /usr/local/tmp/redis-5.0.5/中redis.conf配置文件
1 | # cp redis.conf /usr/local/redis/bin/ |
修改配置文件
1 | # cd /usr/local/redis/bin/ |
把daemonize的值由no修改为yes
5.修改外部访问
在redis5中需要修改配置文件redis.conf允许外部访问。需要修改两处。
注释掉下面
bind 127.0.0.1
1 | #bind 127.0.0.1 |
protected-mode yes 改成 no
6.启动并测试
启动redis
# ./redis-server redis.conf
重启redis
# ./redis-cli shutdown
# ./redis-server redis.conf
启动客户端工具
#./redis-cli
在redis5中客户端工具对命令会有提供功能。
Redis常用的10大类型
Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储,它还支持数据的备份,即master-slave模式的数据备份,同样Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
Redis支持的五大数据类型包括String(字符串 用法: 键 值),Hash(哈希 类似Java中的 map 用法: 键 键值对),List(列表 用法:键 集合 不可以重复),Set(集合 用法:键 集合 可以重复),Zset(sorted set 有序集合 用法: 键 值 值)
String(字符串)
string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。**string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。**
应用场景:
String是最常用的一种数据类型,普通的key/value存储都可以归为此类,value其实不仅是String,
也可以是数字:比如想知道什么时候封锁一个IP地址(访问超过几次)。
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。底层是双端链表,每个列表最多40亿个元素。
应用场景:
Redis list的应用场景非常多,也是Redis最重要的数据结构之一。
我们可以轻松地实现最新消息排行等功能。
Lists的另一个应用就是消息队列,可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。
Hash(哈希)
Redis hash 是一个键值(key=>value)对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
使用场景:存储、读取、修改用户属性
我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息: 用户ID,为查找的key,
存储的value用户对象包含姓名name,年龄age,生日birthday 等信息, 如果用普通的key/value结构来存储,主要有以下2种存储方式:
第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,
如:set u001 “李三,18,20010101”
这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,
如:mset user:001:name “李三 “user:001:age18 user:001:birthday “20010101” 虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。
那么Redis提供的Hash很好的解决了这个问题。
Set(集合)
Redis的Set是string类型的无序集合。
使用场景:1.共同好友、二度好友
2. 利用唯一性,可以统计访问网站的所有独立 IP
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
比如在微博应用中,每个人的好友存在一个集合(set)中,这样求两个人的共同好友的操作,可能就只需要用求交集命令即可。
Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实
实现方式:
set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
zset(sorted set:有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
使用场景:1.带有权重的元素,比如一个游戏的用户得分排行榜
2.比较复杂的数据结构,一般用到的场景不算太多
GEO地理空间
主要用于存储地理位置信息,并对存储的信息进行操作,包括添加地理位置坐标,获取地理位置坐标,计算两个位置的距离,根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
HyperLogLog 基数统计
在输入元素的数量或者体积非常大时,计算基数所需要的空间总是固定并且很小。
比如统计今天天猫访问的访问量,这时候就需要记录不重复的元素的统计。基数统计就是不重复元素的统计
bitmap 位图
里面由许许多多的小格子组成,里面只能放1 和 0 ,用来判断Y/N,用来每日签到,点赞,点击率分析,是否点赞等功能。
bitfield 位域
类似于修改java的字节码,实时数据的替换和查找。
stream流
redis版的mq,消息队列
Redis常用命令
Redis命令相关手册有很多,下面为其中比较好用的两个
1.https://www.redis.net.cn/order/
2.http://doc.redisfans.com/text-in
1. Key操作
keys * 查找所有key
type [key name] 返回类型
unlink key 非阻塞删除,真正的删除在后面异步删除
move key dbindex[0-15] redis默认16个库,然后将当前数据库的key移动到给定的数据库db中
select dbindex 切换数据库【0-15】
dbsize 查看当前数据库的数量
flushdb 清空当前库
flushall 通杀全部库
1.1 exists
判断key是否存在。
语法:exists key名称
返回值:存在返回数字,不存在返回0
1.2 expire
设置key的过期时间,单位秒
语法:expire key 秒数
返回值:成功返回1,失败返回0
1.3 ttl
查看key的剩余过期时间
语法:ttl key
返回值:返回剩余时间,如果不过期返回-1 -2表示已经过期
1.4 del
根据key删除键值对。
语法:del key
返回值:被删除key的数量
2. 字符串值(String)
set 和 get
任何一个value值最大一个512M
1 | set k1 v1 nx 如果不存在k1就创建k1 |
同时设置/获取多个键值
1 | mset k1 v1 k2 v2 k3 v3 |
获取指定区间范围内的值
1 | set k1 abcd1234 |
数值增减
1 | set k1 100 |
获取字符串长度
1 | set k1 abcd |
分布式锁
1 | setnx key value |
什么叫分布式锁,三个微服务,同时争抢一个资源,以前是sync、lock,unlock这些是jvm的,找一个redis,然后找一个节点Key,现在如果一个微服务需要拿资源,需要先去redis上,setnx lock uuid,那么键锁成功,然后执行完del lock,相当于lock和unlock。zookeeper也能只不过那个是建node。
1 | set k1 v1 |
getset
1 | getset k1 haha 就是先get再set |
应用场景
比如抖音无限点赞某个视频或商品,点一下加一次
是否喜欢的文章 用incre
3 哈希表(Hash)
Hash类型的值中包含多组field value。
KV模式不变,V是一个键值对
hset / hget / hmset / hmget / hgetall / hdel
1 | hset user: 001 id 11 name z3 age 25 放了个user:001 然后value 是 id 11等的KV键值对 |
hlen
1 | hlen user:001 |
hexists key 在key里面的某个值的key
1 | hexists user:001 name 是否有name这个字段 |
hkeys / hvals 单独罗列keys 和 vals
1 | hkeys user:001 |
hincrby / hincrbyfloat
1 | hset user:001 age 25 score 99.5 |
hsetnx 值不存在赋值,存在就无效
1 | hsetnx user:001 email 123@123.com |
应用场景
早期的中小厂设计
4. 列表(List)
单key多value,底层是双端链表,容量40多亿,主要功能有push和pop等。
对两端的操作性能高,通过索引下标的操作中间的节点性能会较差。
lpush/rpush/lrange
1 | lpush list1 1 2 3 4 5 |
lpop/rpop
1 | lrange list1 0 -1 5 4 3 2 1 |
lindex 按照索引下标获得元素 从上到下
1 | lindex list1 0 4 |
llen 获取列表中元素的个数
lrem key 数字N 给定值v1 解释(删除N个值等于v1的元素)
1 | list里面是允许有重复元素的,如果要去重 |
ltrim key 开始index 结束 index 解释 截取指定范围的值后再赋值给key
1 | lrange list1 0 -1 5 4 3 3 2 2 2 1 |
rpoplpush 原列表 目标列表
1 | rpoplpush list2 list3 从list2除了14 |
消息队列了可以当做
lset key index value
1 | lset list1 1 mysql 在list下标1设mysql |
linset key before/after 已有值 插入的新值
1 | linsert list1 before mysql java 在mysql前面加 |
应用场景
微信公众号订阅文章
1、作者发布了文章11和22
2、关注了他们的文章装进list,lpush likearticle 11 22
3、查看自己订阅的文章 lrange likearticle
5 集合(Set)
set和java中集合hashset一样。单值,多value,无重复
sadd key menber / smembers key / sismember key member
1 | sadd set1 1 1 1 2 2 2 3 4 5 这自动去重,只加了5个元素 |
srem set1 删除元素
1 | srem set1 1 |
scard set1 统计元素个数
srandmember key [数字] 从集合中随机展现设置的数字个数元素,元素不删除
spop key [数字] 从集合中随机弹出一个元素,出一个删一个
smove key1 key2 【key1里的value】在key1里已存在的某个值赋给key2
集合运算 差并交
1 | sadd set1 a b c 1 2 |
sintercard
redis7新命令,他不返回结果集,只返回结果的基数。返回由所有给定集合的交集产生的集合的基数
交集的个数其实
1 | sintercard 2 set1 set2 求2个key 分别是set1和set2,然后求去重后的交集个数 |
应用场景:你可能认识的人,社交,推荐等
6 有序集合 zset
在set基础之上,每个val值前加一个score分数值,排行榜用这个数据结构很方便的
zadd / zrange / zrevrange / zrangebyscore
1 | zadd zset1 60 v1 70 v2 80 v3 90 v4 100 v5 |
zscore / zcard / zrem / zincrby / zcount / zmpop
1 | zscore zset1 v1 获取元素的分数 |
zrank / zrevrank
1 | zrank zset1 v2 0 获得zset1的v2的下标值 zrank是0 正序的0 |
应用场景
热销的商品,热销的主播打赏
7 位图 bitmap
由 0 和 1 表现的二进制的 bit 数组
钉钉打卡上下班,电影、广告是否被点击播放过,用户是否登陆过,签到统计
一个字节(一个byte) 8位,底层是个String类型的数组,每个二进制位对应一个索引。
Bitmap支持的最大位数是2 的32次方位,用512M内存能存储40多亿数据
setbit / getbit
1 | setbit k1 1 1 |
strlen
1 | strlen k1 这时候计算的是字节为单位,不满一个字节算一个字节 |
bitcount
1 | bitcount k1 记录1的个数 |
bitop
统计连续2天签到用户,做个id和位置的映射
1 | hset uid:map 0 uid1 用户表 |
8 基数统计 hyperloglog
底层是string
统计某个网站的uv,统计某个文章被阅读的uv
uv就是独立访客,一般认为是ip,union vister,同一个ip就是同一个客户
还有用户搜索网站关键词的数量,统计用户每天搜索不同词条个数
在redis,每个hyperloglog健只需要花费 12kb内存,就可以存储2的64次方个不同的元素的基数。32次方就40亿了。
pfadd / pfcount / pfmerge
1 | pfadd hll01 1 3 4 5 7 9 在hll01加元素 1 3 4 5 7 9 |
9 地理空间 GEO
地球上的地理位置是二维的经纬度表示,然后确定一个经纬度就可以确定位置经度(-180,180] , 维度(-90,90],但是如果要实现找附近的人,如果用二维坐标,误差会很大,并且范围查找会很慢。
核心思想就是将球体转换为平面,平面转换为一个点。
主要分三步,将三维的地球转变为二维的坐标,二维变一维,最后将一维点转换二进制base32编码
底层是zset
geoadd 添加经度维度坐标
1 | geoadd [key] [经度] [维度] [位置名称] 具体的经纬度百度获取 |
geopos 返回经纬度
1 | geopos city 天安门 故宫 长城 然后就会返回这三个地点的经纬度 |
grohash 返回坐标的hash表示
小数点后面很多位,所以在写程序的时候非常不好表示,所以用个映射表示对应的经纬度
1 | geohash city 天安门 故宫 长城 然后经纬度就变成字符串了 |
geodis 两个位置的距离
1 | geodis city 天安门 长城 km 后面的单位可以更换,返回距离 |
georaduis 以半径为中心查找附近的xxx
这里返回的是与中心的距离不超过给定最大距离的所有位置元素
1 | georadius city [自己的经度] [自己的维度] 10 km withdis withcoord count 10 withhash desc |
georadiusbymember 跟上面类似,但是这个不需要知道经纬度,只要知道地点名称即可
1 | GEORADIUSBYMEMBER city "北京" 200 km 10 km WITHDIST WITHCOORD COUNT 10 WITHHASH DESC |
10 redis 流 stream
redis的stream就是redis版本的MQ,消息中间件。
MQ 有很多 kafka RabbitMQ rocketmq
redis消息队列有2个方案,用List实现 lpushrpop 如果消息队列及其简单用redis就能解决
通常用作异步队列,通信这块,点对点。但是一对多力不从心
然后就有pub和sub
但是消息无法持久化,网络断开、redis断开,消息就会丢弃,也没有ack机制来保证数据的可靠性。
基于此上面这些痛点,新增了Stream希望能解决这些问题
所以stream流就是消息中间件 + 阻塞队列
steam流支持消息队列,消息持久化,支持自动生成全局唯一ID,支持ack确认消息的模式,支持消费组模式,让消息队列更加可靠和稳定
stream的结构
1、message content 消息内容
2、消费组 同一个消费组有多个消费者
3、last_delivered_id 游标,每个消费组有个游标,任何一个消费者读取了消息会使游标往后移动
4、consumer 在消费组中的消费者
5、pending_ids 消费者会有个状态变量,记录当前消费但是没ack的消息的id,如果客户端没有ack,这个消息id会越来越多,一旦ack才会减少。
命令
xadd 添加消息到队列末尾
消息id必须比上一个id大,默认用星号表示自动生成id,类似于自增主键
1 | xadd mystream * id 11 cname z3 |
xrange
1 | xrange mystream - + 把消息队列从小到大返回 |
xrevrange
1 | xrevrange mystream + 1 意思是消息队列从大到小倒序返回 |
xdel
1 | xdel mystream [时间] 是按照id删也就是主键删 |
xlen
1 | slen mystream 返回消息个数也就是key的个数 |
strim
1 | xtrim mystream maxlen 2 也就是按照最大长度 截取2条信息如果有四条信息,那么只会剩下下面两条 |
xread
block没写非阻塞,否则是阻塞
1 | xadd mystream * k2 v2 |
消费组的相关指令
首先$ 表示从Stream尾部开始消费
0表示从Stream头部开始消费
创建消费者组的时候必须指定 ID,ID为0表示从头开始消费,为 $ 表示只消费新的消息,队尾新来
xgroup create
1 | xgroup create mystream groupX $ |
xreadgroup group
1 | > 表示从第一条尚未被消费的消息开始读取 |
重点问题
基于Stream实现的队列,如何保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息?
刚才只是读了但是还没ack,还没签收,Streams会自动使用内部队列也称为PENDINGList 留存消费组里每个消费者读取的消息保底措施,直到消费者使用XACK命令通知Streams 消息已经处理完成
xpending
1 | xpending mystream groupc 看看消费组里已读未确认的清单 |
XACK
1 | XACK mystream groupc [时间戳] 这样就确认签收了 |
11 位域 bitfield
将一个redis字符串看做是一个二进制组成的数组,并能对变长位宽和任意没有字节对齐的指定整形位域进行寻址和修改
Redis持久化策略
Redis不仅仅是一个内存型数据库,还具备持久化能力。
持久化两个策略RDB AOF,RDB就是做一个快照,AOF就是把写操作都记录在文档里
1. RDB
以指定的时间间隔执行数据集的时间点快照,即便redis故障,快照文件也不会丢失,数据的可靠性得到保证。这个快照以dump.rdb。保存的是 全量快照
redis6以下,自动触发,每个900s,如果有超过1个key发生变化,就写一份新的RDB文件
每隔300s,如果有超过10个key发生了变化,就写一份新的RDB文件
每隔60s,如果超过10000个key发生了变化,就写一份新的RDB文件
redis7以后,3600s以内,1个修改,300s以内100个修改,60s内10000个修改
rdb的操作分为自动触发和手动触发
这里修改配置文件来自定义
下面是修改配置文件的写法,位置自己找,以后做redis集群需要
1 | save 5 2 修改快照频率这个5秒内如果有累计2次修改以上包括两次就会触发,如果增加k3然后5秒内不会新增,然后5秒后新增k4之后也会保存。 |
在redis-cli内可以通过下面的命令查看redis信息
1 | config get requirepass 查看密码 |
rdb模式是默认模式,可以在指定的时间间隔内生成数据快照(snapshot),默认保存到dump.rdb文件中。当redis重启后会自动加载dump.rdb文件中内容到内存中。
恢复
将备份文件移动到redis安装目录并启动服务即可。也就是说将备份文件放到配置里面备份的路径,redis启动的时候会自动恢复
首先如果shutdown就会保存一次,并且调用flushall的时候也会更新到dump.db里面
所以不可以把备份文件和生产redis服务器放在同一台服务器,必须分开各自存储,以防备份文件也没了
手动触发 save / bgsave
redis会forks ,然后就会有个子的和父进程。然后后台子进程会覆盖rdb了。
生产上只允许用bgsave不许用save
因为save会阻塞当前redis服务器,直到持久化工作完成,redis不能执行其他命令。
1 | save |
优点和缺点
适合大规模的数据恢复,定时备份,内存加载速度快,对一致性要求不高。
rdb在没有正确关闭的时候会丢失部分数据。fork可能会很耗时如果数据量大。
检查并修复rdb文件
1 | cd /usr/local/bin 类似于programfile然后里面有redis-check |
哪些情况会触发RDB快照
1、配置文件中默认的快照配置
2、手动save/bgsave
3、执行flushall / flushdb 命令但里面是空的无意义
4、执行shutdown且没有设置开启AOF持久化
5、主从复制时,主节点自动触发
rdb优化配置项
1 | save <seconds> <changes> |
如何禁用快照
1 | 动态的停止RDB redis-cli config set save "" 本次cli禁用 |
2 AOF
AOF(Append Only File)把Redis执行过的所有写指令记录下来,读操作不记录、redis启动之初会读取该文件重新构建数据。
默认情况下,redis没有开启,要开启需要在配置文件中添加appendonly yes
1 | # 默认no |
AOF工作流程
AOF三种写回策略
Always / everysec / no
1 | appendfsync everysec 在配置文件中,每秒写回,先把日志写到AOF缓冲区,每隔1秒写回aof文件中 |
AOF的配置文件说明
Redis7新特性 Multi-part AOF
aof的保存路径
redis6是 AOF 保存文件的位置和 RDB 文件位置是一样的,都是dir中配置的
1 | dir /myredis |
redis7是 有dir + appenddirname
1 | dir /myredis |
aof保存的名称
redis6 只有叫appendonly.aof
redis7 Muti Part AOF设计,多部分组成aof 对外名字还叫appendonly.aof 但是实际上分为了三个文件组成
base 基本文件
incr 增量文件
manifest 清单文件
尝试利用 AOF 恢复流程
写操作继续,然后生成AOF文件到指定目录
恢复1 : 重启redis然后重新加载,结果OK
恢复2:写入数据进redis,然后flushdb+shutdown服务器,新生成了dump和aof,备份新生成的aof.bak,然后删除dump/aof再看恢复,重启redis然后加载,停止服务器,拿出我们的备份修改后再重启服务器
上面都是正常状况
异常恢复 假设内容只写了一小半,没写完整,redis挂了,导致aof文件错误
1 | 利用命令 redis-check-aof --fix 进行修复 |
aof重写机制
当aof的文件越来越大,不断记录写命令,AOF恢复要求的时间越来越长。
只保留可以恢复数据的最小指令集,或者手动使用命令来重写
1 | 自动的话配置,同时满足且的关系才会触发 |
手动的话客户端向服务器发送bgrewriteaof 命令
AOF文件的重写并不是对源文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。
重写原理
Redis 7 的 AOF 重写通过 fork 子进程将内存数据转为紧凑命令写入 base 文件,父进程将重写期间的新命令写入新的 incr 文件,最后通过 manifest 清单将 base + incr 组合成逻辑上的完整 AOF,全程无需覆盖或删除任何现有文件。
Redis 7 的 AOF 重写全程在新文件上操作,旧 AOF 文件保留且不被覆盖。
旧文件在不再被 manifest 引用后,由 Redis 后台异步清理,确保高可用与数据安全。
总结配置
3 RDB + AOF 混合
如果开启了aof,会优先加载aof,其次再加载rdb文件来恢复。
开启混合配置
1 | aof-use-rdb-preamble yes |
这时候产生的文件一部分是rdb,一部分是aof。
如果用纯缓存模式,同时关闭RDB和AOF
1 | 在配置里面 |
Redis 事务
事务就是一组命令的集合,要么一起成功,要么一起失败。
在一个队列中,一次性,顺序性,排他性的操作
Redis事务和数据库的事务区别
1、Redis事务是单独的隔离操作:Redis事务仅仅保证事务的操作会被连续独占执行,执行完事务内的所有指令不可能再去同时执行其他客户端的请求。
2、没有隔离级别的概念:因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题
3、Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力
4、排他性:Redis会保证一个事务内的命令一次执行,而不会被其他命令插入
但是使用 Redis 事务 确实存在数据丢失或数据不一致的风险,mysql不会只要做好配置。
常用命令
Discard 取消事务,Exec 执行所有事务块内的命令
Multi 标记一个事务块的开始
Unwatch 取消watch命令对所有key的监视
Watch key 监视一个或多个key,如果事务执行之前这些key被其他命令所改动,那么事务将被打断
case1 正常执行
1 | multi |
case2 放弃执行
1 | multi |
case3 全体连坐
1 | 如果事务出错全部不执行,这个是在执行exec之前才会发生的 |
case4 冤头债主
1 | 如果事务exec后执行异常,一个命令失败,其他命令还是继续执行 |
case5 watch监控
1 | Redis使用Watch来提供乐观锁定,类似于CAS。 |
Redis管道
处理问题的结果跟事务类似,但是完全不一样。
如何优化频繁命令往返造成的性能瓶颈?Redis每秒钟可以8万次写,10万次读,但是一条命令需要返回一个OK,这次想能不能批处理这时候想到mset,这个mset就类似于管道,把所有命令排成一个流水线,一次性操作。
解决思路
然后就引出了管道的概念,一次发送多条命令给客户端,只有一次相应,鉴赏通信次数,实现降低往返延时时间。
案例
1 | 首先写一个cmd.txt然后里面写着 |
管道与原生批量命令对比
原生就是mset那些。首先原生批量命令是原子性的,但是管道不是原子性的,其次原生批量命令一次只能执行一种类型,管道可以各种类型的命令,原生是服务端实现的,管道是服务端和客户端共同实现的。
管道与事务
事务具有原子性,管道不具有
管道一次性将多条命令发送到服务端,事务是一条一条发收到exec命令才会执行
执行事务会阻塞其他命令的执行,管道中的命令不会
使用管道注意的事项
管道缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令
使用管道组装的命令不能太多没不然数据量过大,客户端阻塞的时间可能会过久。
Redis 发布订阅
不推荐使用。
Redis主从复制
HA high application 高可用
master以写为主,slave以读为主。
当master数据变化的时候,自动将新的数据异步同步到其他slave数据库
能干嘛?
1、读写分离
2、容灾恢复
3、数据备份
4、水平扩容支撑高并发
怎么用
配从库不配主库,一个主库,两个从库
权限细节
单机配置了requirepass,需要密码登录,那么slave就要配置masterauth来设置校验密码,否则master就会拒绝slave的访问请求
1 | masterauth [密码] |
常用操作命令
1 | info replication 可以查看复制节点的主从关系和配置信息 |
一主多从搭建
3台虚拟机,都安装redis
1 | 拷贝多个redis.conf文件,分别为redis6379.conf、redis6380.conf、redis6381.conf |
首先保证三边的网络相互ping通,且注意防火墙配置
1 | ifconfig 查看ip |
配置从redis为例子
1 | 开启daemonize yes 后台运行yes,大概309行 |
搭建好之后
1 | 先启动主,后两台从再开启 |
从机可以写命令吗?不可以
那主机写到了k3然后从机启动了,这时候从机启动,从机会读k1和k2吗?会
主机shutdown,从机会当主机吗?不会,从机不动原地待命
主机shutdown后,重启后,能恢复吗?能
从机shutdown后,master继续,从机恢复能跟上吗?能
薪火相传
上一个slave可以是下一个slave的master,slvae同样可以接受其他slaves的连接和同步请求,那么该slave可以减轻主master的写压力。
中途变更转向:会清除之前的数据,重新建立拷贝最新的。slaveof 新主库ip 新主库端口
这种配置一个主master,然后一个slave,一个slave的slave,这样首先主master写操作,两个slave都能读到,然后这时候两个slave还是不能写操作。
反客为主
用slaveof no one然后自己就变成master了
面试题复制原理和工作流程
slave启动,同步初请:首先slave连接到master后会发送一个sync命令,初次连接master,第一次完全同步全量复制将自动执行,slave自身原有的数据会被master数据覆盖清除。
首次连接,全量复制:master节点收到sync命令后会开始在后台保存快照,即RDB持久化,主从复制时会触发RDB,同时收集所有接收到的用于修改数据集命令缓存起来,master节点执行RDB持久化完后,master将rdb快照文件和所有缓存的命令发送到所有slave,完成一次完全同步。
而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化。
心跳持续,保持通信: master发出PING包周期,默认是10秒,每十秒中确认是否连接还在。
进入平稳,增量复制:Master继续将新的所有收集到的修改命令自动依次传给slave,完成同步。
从机下线,重连续传:假设有一台机器down了,master会检查backlog里面的偏移量offset,master和slave都会保存一个复制的offset还有一个masterId,offset是保存在backlog中的。Master只会把已经复制的offset后面的数据复制给slave,类似断点续传
复制的缺点
复制延时,信号衰减
master挂了怎么办
默认情况下,不会在slave里面选一个master,那每次都要人工干预?
无人值守安装成了刚需,那么就引出了哨兵和集群。
哨兵(Sentinel)
在redis主从默认是只有主具备写的能力,而从只能读。如果主宕机,整个节点不具备写能力。如果故障了根据 投票数 自动将某一个从库转换为新主库,继续对外服务。
作用:无人值守运维,监控redis运行状态,当master宕机能自动将slave切换成新master
能干嘛?
1、主从监控:监控主从redis库运行是否正常
2、消息通知:哨兵可以将故障转移的结果发送给客户端
3、故障转移:如果Master异常,则会进行主从切换,将其中一个Slave作为新Master
4、配置中心:客户端通过连接哨兵来获得当前Redis服务的主节点地址
Redis Sentinel架构
1、3个哨兵:自动监控和维护集群,不存放数据,只是吹哨人
2、1主2从:用于数据读取和存放
6台机器。
实战步骤
1 | /myredis目录下新建或者拷贝sentinel.conf文件,名字绝不能错 |
一些其他的参数
哨兵运行流程和选举原理
当一个主从配置中的master失效之后,sentinel可以选举出一个新的master用于自动代替原来master的工作,主从配置中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。
详细
1、三个哨兵监控一主二从,正常运行中
2、SDOWN 主观下线。单个sentinel自己主观上检测到的关于master的状态,从sentinel角度来看如果发送了PING心跳后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件
sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度
3、ODOWN 客观下线。ODOWN需要一定数量的sentinel,多个哨兵达成一致意见才能认为一个master客观上已经宕掉。
默认30秒
4、选一个领导者哨兵,然后由该哨兵来进行故障迁移(switch master 添加slave)
选举是通过Raft算法:监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法,这个算法的基本思路是 先到先得 即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵就会同意A成为领导者
5、由领导者来推动切换流程,并选出一个新master
首先新master
某个Slave被选中成为Master,选出新Master的规则,先看优先级数字越小越高,谁高谁当master,再看复制偏移量(谁复制的多),谁大谁当,最后看run id ,谁最小谁当Run ID 是 Redis 实例启动时自动生成的唯一标识符
然后slave切换master
被选中的 slave 会执行slaveof no one ,然后通过slaveof命令让其他从节点变成他的从节点。Sentinel leader 会对选举出的新master执行slaveof no one操作,将其提升为master节点,然后向其他slave发送命令,让剩余的slave成为新的master节点的slave
最后旧master认新master
之前已经下线的老master设置为新选出的新master的从节点,当老master重新上线后,他会成为新Master的slave。Sentinel leader 会让原来的master降级为slave并回复正常工作
redis配置注意事项
哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用。
哨兵节点的数量应为奇数。
各个哨兵的配置应该一致
如果哨兵布置在Docker容器里面,要注意端口的正确映射
哨兵集群 + 主从复制,并不能保证数据零丢失
那么由于不能保证数据零丢失,那么就引出下面的集群了
集群(Cluster)
先回顾哨兵 + 主从
这里有个Bug就是当主机挂了重新选举的时候,写操作被中断,一瞬间写操作数据流失、所以单点高并发只有一台主机,所以单个Master难以承担,因此需要多个复制集进行集群。
现在就变成集群了。Redis集群是共享数据的,M1有数据,M2,M3也会有,写的时候全局数据共享形成程序集。
能干嘛
1、Redis集群支持多个Master,每个Master又可以挂载多个Slave
2、由于集群自带Sentinel的故障转移机制,内置了高可用支持,所以 无需再去使用哨兵功能
3、客户端与Redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可。
4、 槽位Slot 负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系
集群算法-分片-槽位slot
redis集群的槽位slot
Redis 集群没有使用一致性hash,而是用hash槽的概念,槽位最多16384,集群要1000个以内。
那么进行写操作的时候,会对每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
举个例子:比如当前集群有三个节点那么
分片
使用Redis集群时,我们会将存储的数据分散到多态redis机器上,这成为分片。简言之,急群众的每个Redis实例,都被认为是整个数据的一个分片。
如何找到给定key的分片:就是你写了之后怎么找到写到哪。为了找到,我们会对key进行CRC16算法处理并通过对总分片数量取模,然后,使用确定性哈希函数,这意味着给定的key 将多次始终映射到同一分片 我们可以推断将来读取特定key的位置。
那这两个概念的引入有什么优势
1、方便找
2、方便扩容和缩容:在增加master或者减少master的过程中只是运了一部分槽给新增的或者减少的master而已,并不会停机,这就是高可用
slot槽位的映射
1、哈希取余分区 (小厂):直接hash(key) % N个机器数,简单粗暴,直接有效。但是原来规划好的节点,进行扩容或者缩容就比较麻烦了,因为分母写死了,映射关系需要重新计算,此时取余计算的结果发生很大变化,全部数据非常混乱,重新洗牌变更。
2、一致性哈希算法分区 (中厂)
为了解决分母数量变动取余不行的问题。当服务器个数变动时,尽量减少映射的影响。
1、算法构建一致性哈希环
现在有个hash函数并按照算法产生hash值,这个算法的所有可能哈希值会构成一个全量集合,这个集合可以成为一个hash空间,这是一个线性空间,但是在算法中,我们通过适当的逻辑控制将他首尾相连,这样让他在逻辑上形成了一个环形空间。
他也是按照取模的方法。前面是对服务器数量取模,而一致性hash算法是对2的32次方取模,整个空间 按顺时针方向组织
2、服务器IP节点映射
将集群中各个IP节点映射到环上的某一位置。具体可以用IP或者主机名作为关键字进行哈希
3、key落到服务器的落键规则
当我们需要存储一个kv键值对时,首先计算Key的hash值,hash(key) ,将这个key使用相同的函数hash计算处哈希值并确定此数据在环上的位置。 从此位置沿环顺时针行走,第一台遇到服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。
优点: 容错性!扩展性!
容错性
这里C服务器挂了,C节点继续往前走到了D。这时候A\B\D是不受影响的,如果一台服务器不可用,受影响的数据仅仅是此服务器到其换空间中前一台服务器之间的数据(也就是逆时针走遇到第一台服务器),其他的不会受到影响。简单说就是C挂了,收到影响的只是B\C之间的数据,且这些数据会转移到D进行存储
扩展性
数据量增加,增加一台节点NodeX,X位置在AB之间,那受到影响的也就是A到X之间的数据,重新吧A到X的数据录入到X上即可,不会导致hash取余全部数据重新洗牌。
缺点:一致性hash算法的数据倾斜问题
当服务节点太少的时候,容易因为节点分布不均匀而造成的数据倾斜问题。
3、哈希槽分区 (大厂)
是什么?CRC16(KEY) mod 16384 = HASH_SLOT
为了解决数据倾斜问题。哈希槽其实是2的16次方也就是16384的数组
能干什么?
解决均匀分配问题,他在数据和节点之间又加入了一层,这层成为hash槽,用于管理数据和节点之间的关系。现在就相当于节点上放的是槽,槽里放的是数据。
槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动,哈希解决的的是映射问题,使用Key的哈希值来计算所在的槽,便于数据分配
多少hash槽?
一个集群只能有16384个槽,编号从0 开始,这些槽会分配给集群中的所有主节点,分配策略没有要求。
集群会记录节点和槽的对应关系,解决了节点和槽的关系后,接下来就需要对key求哈希值,然后对16384取模,然后对16384取模,余数是几,key就落入对应的槽里。HASH_SLOT = CRC16(key) mod 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样移动问题就解决了。
为什么redis集群的最大槽数是16384个
1、如果槽位是65536,发送心跳消息的消息头达8K,发送的心跳包过于庞大。
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位是65536,这个ping消息的消息头太大了,浪费带宽。这个ping消息是以bitmap也就是01的map形式传递
- 如果有 65536 个槽,那么位图长度为:
65536÷8=8192 字节=8 KB - 而 16384 个槽 对应的位图长度为:
16384÷8=2048 字节=2 KB
2、redis的集群主节点数量基本不会超过1000个
集群节点越多,心跳包的消息体内携带的数据越多。如果节点超过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没必要扩展到65536个。
3、槽位越小,节点少的情况下,压缩比高,容易传输
Redis主节点的配置信息中他所负责的哈希槽是通过一张bitmap的形式来存储的,在传输过程中会对bitmap进行压缩,但是如果Bitmap的填充率 slots / N 很高的话,bitmap的压缩率就很低。如果节点数很少,哈希槽数量很多的话,bitmap的压缩率就很低。也就是说尽量让槽位和节点数接近是最大效率的,那么节点数由于不会超过1000那么16384就刚好了。
Redis不保证强一致性,也就是在特定情况下,还是会丢失一些写命令
集群环境搭建
1、3主3从redis集群配置
1 | ip + 端口 这时候让一主一从在一台服务器上,也就是这时候模拟3台服务器形成的集群。 |
2、3主3从读写
1 | 这时候写 |
3、主从容错切换迁移
1 | 主6381宕机,从机会不会上位 |
4、主从扩容
1 | 新启动两个redis实例,然后利用redis-server启动两个实例,这时候两个都是master |
5、主从缩容
1 | 首先清除从节点 |
集群常用操作命令和CRC16算法分析
1、不在同一个slot槽位下的多键操作支持不好,通识占位符登场
1 | 在一个redis里面 |
2、Redis集群里面有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。
集群的每个节点负责一部分hash槽。
3、常用命令
1 | 集群是否完整才能对外提供服务 cluster-require-full-coverage |
SpringBoot集成Redis
之前java连接mysql用的驱动包是jdbc,那么java连接redis也有自己的一套驱动包。
总体概述
jedis-lettuce-RedisTemplate三者的关系
jedis 相当于jdbc,最初的驱动包
lettuce 对jedis优化
最后集成RedisTemplate
本地Java连接Redis常见问题
1 | bind配置注释掉 否则相当于windows系统连接linux系统的 |
集成Jedis
Jedis Client是Redis官网推荐的一个面向java客户端,库文件实现了对各类API进行封装调用
也就是之前用的所有十大类型和命令,可以通过java程序进行调用了
1 | 约定大于配置大于编码,编码放在最后 |
1 | //这里是springboot然后这里用来了解Jedis的所以这里不用@Component,用最原始的人工控制 |
集成lettuce
为什么出现这个?
如果在高并发的环境下,连接jedis每次都new一个客户端,绝对机器承担不住,这样就想到了池化技术。
jedis也支持池化技术,但是lettuce更好的封装所以以后就用lettuce但是被springboot集成了,所以以后就用springboot了。
为什么用lettuce:主要是lettuce底层使用的是Netty,jedis客户端连接Redis服务器的时候,每个线程都要自己创建jedis实例去连接Redis客户端,当有很多个线程的时候,不仅开销大需要反复的创建关闭一个Redis连接,而且也是线程不安全的(一个线程通过Jedis实例更改Redis服务器中的数据之后会影响另一个线程)。
使用Lettuce这个客户端连接Redis服务器的时候,就不会出现上面的情况,当有多个线程都需要连接Redis服务器的时候,可以保证只创建一个Lettuce连接,使所有线程都共享这一个Lettuce连接,这样可以减少创建关闭一个Lettuce连接时候的开销。这种方式也是线程安全的,不会出现一个线程通过Lettuce更改Redis服务器中的数据之后而影响另一个线程的情况。
1 | 步骤跟上面一样就是改POM文件然后用lettuce的业务类就可以了 |
集成RedisTemplate
连接单机
1 | 首先boot整合redis基础 |
OrderService
Controller
但是存入的key和读取的key都乱码,需要注意序列化问题
1 | key和value 都是通过Spring提供的Serializer序列化传到数据库的 |
连接集群
1 | 启动redis集群6台实例 |











































