入门:
介绍:
属于NoSQL数据库的一种,Not Only SQL
键值(Key-Value)存储数据库:字典的键用的哈希算法,key唯一
列存储数据库:查询快
文档型数据库:
图形(Graph)数据库:
redis是业界主流的key-value nosql 数据库之一。和Memcached类似
各个消息队列比较:
kafka是Linkedin于2010年12月份开源的消息发布订阅系统,它主要用于处理活跃的流式数据,大数据量的数据处理上。 支持分组,负载均衡。CP模型。
RabbitMQ,遵循AMQP协议,由内在高并发的erlanng语言开发,用在实时的对可靠性要求比较高的消息传递上,对性能和吞吐量的要求还在其次。CP模式。
redis 消息推送(基于分布式 pub/sub)多用于实时性较高的消息推送,并不保证可靠。速度快。AP模型。
redis为什么不可靠?
不保证强一致性
集群存在丢失数据的场景(主从互备不能保证数据一致性):
异步复制(用了集群异步复制. 写操作过程)
在 master 写成功,但 slave 同步完成之前,master 宕机了,slave 变为 master,数据丢失。
wait 命令可以给为同步复制,但也无法完全保证数据不丢,而且影响性能。
网络分区(集群出现了网络分区)
分区后一个 master 继续接收写请求,分区恢复后这个 master 可能会变为 slave,那么之前写入的数据就丢了。
单节点的场景:
1.无法以事务的形式写AOF文件和执行写操作。一旦机器在写AOF文件和执行写操作中间的某一时刻崩溃,都会导致数据的不一致性。
Mysql使用二阶段提交解决这个问题。prepare阶段写rodo-log并将其标记为prepare状态,commit阶段:写bin-log并将其标记为commit状态。
2.文件同步到磁盘过程并非原子操作。
mysql同步磁盘使用”double write”解决这个问题。
对比kafka:是否可靠?
kakfa依靠主从+硬盘的机制,而redis是写到内存,然后系统自动进行AOF
rabbimaq有内存模式和硬盘模式,硬盘模式能实现强一致性。
版本升级:
6.0.7版本已知有bug,推荐6.0.8修复版本。
设计架构:
1.基于内存
和memcached一样,为了保证效率,数据都是缓存在内存中,节省了磁盘IO。
区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,持久化RDB定时保存\AOF记录所有命令。
并且在此基础上实现了master-slave(主从)同步。
2.单线程
Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销
3.运用IO多路复用epoll
Redis使用多路复用IO技术,在poll,epool,kqueue选择最优IO实现
4.优化的数据结构:
Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能
安装与部署:
命令:
wget http:
tar xzf redis-5.0.5.tar.gz
cd redis-5.0.5
make
make install
底层数据结构:
所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中
typedef struct redisDb {
int id; id是数据库序号,为0-15(默认Redis有16个数据库)
long avg_ttl; 存储的数据库对象的平均ttl(time to live),用于统计
dict *dict; 存储数据库所有的key-value,每个键值对都会有一个dictEntry
dict *expires; 存储key的过期时间
dict *blocking_keys; blpop 存储阻塞key和客户端对象
dict *ready_keys; 阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象
dict *watched_keys; 存储watch监控的的key和客户端对象
} redisDb;
value存放于RedisObject结构
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
void *ptr;
int refcount;
unsigned lru:LRU_BITS;
}robj;
应用场景:
1.DB缓存,减轻DB服务器压力。
2.提高系统响应
3.做Session分离
4.做分布式锁(Redis)
5.做乐观锁(Redis)
运维命令:
开启:
redis-server 指定配置文件启动./redis-server ../redis.conf
进入命令行:
redis-cli
查命令:
help xxx
重启:
service redis restart --protected-mode no
ubutun中
/etc/init.d.redis-server restart
保护模式:
config set protected-mode "no"
关闭:
service redis stop
redis-cli -h 127.0.0.1 -p 6379 -a root shutdown
设置密码和ip:
vi /etc/redis.conf
rm .*.swp
bind 0.0.0.0
protected-mode no
requirepass root
port 6379
后台启动:
daemonize yes
认证:
auth root
数据类型:
String:
概述:
redis中的String在在内存中按照一个name对应一个value来存储。
value可以是字符串和整型
底层结构:
int、raw(大字符串 长度大于44个字节)、embstr(小字符串 长度小于44个字节)
应用:
分布式锁setnx
点赞数量incr
商品号、订单号incr
相关命令:
SET key value 设置值,默认,不存在则创建,存在则修改
GET key 获取键key对应的值
GETSET key value 设置新值并返回旧值
MGET key1 [key2..] 批量设置(k1='v1', k2='v2')或者字典
MSET key value [key value...] 设置多个键和多个值
SETEX key seconds value 键到期时设置值
PSETEX key milliseconds value 设置键的毫秒值和到期时间
SETNX key value 设置值,只有name不存在时,执行设置操作(添加),可用于实现分布式锁
MSETNX key value [key value...] 设置多个键多个值,只有在当没有按键的存在时
STRLEN key 得到存储在键的值的长度
SETRANGE key offset value 从指定字符串索引开始向后替换(新值太长时,则向后添加)
GETRANGE key start end 切片,包含end,-1是最后
GETBIT key offset 返回存储在键位值的字符串值的偏移
SETBIT key offset value 对key对应值的二进制表示的位进行操作,用途:位图,将key hash16进制转10进制。
BITCOUNT key start end 获取name对应的值的二进制表示中 1 的个数
INCR key 增加键的整数值一次
INCRBY key increment 由给定的数量递增键的整数值
INCRBYFLOAT key increment 由给定的数量递增键的浮点值
DECR key 递减键一次的整数值
DECRBY key decrement 由给定数目递减键的整数值
APPEND key value 追加值到一个键
SCAN cursor [MATCH pattern] [COUNT count] cursor表示offset
Hash:
概述:
一个key对应一个字典,Map<string,Map<k,v>>
应用:
博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
购物车(field为proId,value为数量,hlen获取购物车总数,key为shopcar:uidxxxx,点击+号hincrby,hgetall获取全部)
底层结构:
字典和压缩列表
相关命令:
HSET key field value 设置对象指定字段的值
HGET key field 获取对象中该field属性域的值
HDEL key field[field...] 删除对象的一个或几个属性域,不存在的属性将被忽略
HMGET key field[field...] 获取对象的一个或多个指定字段的值
HMSET key field value [field value ...] 同时设置对象中一个或多个字段的值
HEXISTS key field 查看对象是否存在该属性域
HGETALL key 获取对象的所有属性域和值
HKEYS key 获取对象的所有属性字段
HVALS key 获取对象的所有属性值
HLEN key 获取对象的所有属性字段的总数
HINCRBY key field value 将该对象中指定域的值增加给定的value,原子自增操作,只能是integer的属性值可以使用。应用:用于计数器,统计文章访问量,一次更新到数据库
HINCRBYFLOAT key field increment 将该对象中指定域的值增加给定的浮点数
HSTRLEN key field 返回对象指定field的value的字符串长度,如果该对象或者field不存在,返回0.
HSETNX key field value 只在对象不存在指定的字段时才设置字段的值
HSCAN key cursor [MATCH pattern] [COUNT count] 类似SCAN命令,增量式迭代获取,对于数据大的数据非常有用,hscan可以实现分片的获取数据,并非一次性将数据全部获取完,从而放置内存被撑爆
List:
概述:
List操作,redis中的List在在内存中按照一个name对应一个List来存储。
底层结构:
quicklist
应用:
订阅公众号列表翻页,通过list维护每个人最新发布文章的公众号,按照时间排序,通过lrange翻页。
相关命令:
LPOP key 获取并取出列表中的第一个元素
LPUSH key value1 [value2] 在前面加上一个或多个值的列表
RPOP key 取出并获取列表中的最后一个元素
RPUSH key value1 [value2] 添加一个或多个值到列表
BLPOP key1 [key2 ] timeout 取出并获取列表中的第一个元素,或阻塞,直到有可用
BRPOP key1 [key2 ] timeout 取出并获取列表中的最后一个元素,或阻塞,直到有可用
RPOPLPUSH source destination 删除最后一个元素的列表,将其附加到另一个列表并返回它
BRPOPLPUSH source destination timeout 从列表中弹出一个值,它推到另一个列表并返回它;或阻塞,直到有可用
LPUSHX key value 在前面加上一个值列表,仅当列表中存在
RPUSHX key value 添加一个值列表,仅当列表中存在
LINDEX key index 从一个列表其索引获取对应的元素
LINSERT key BEFORE|AFTER pivot value 在列表中的其他元素之后或之前插入一个元素
LLEN key 获取列表的长度
LRANGE key start stop 从一个列表获取各种元素,用途:做基于redis的分页功能,性能极佳,用户体验好。
LREM key count value 从列表中删除元素
LSET key index value 在列表中的索引设置一个元素的值
LTRIM key start stop 修剪列表到指定的范围内
Set:
概述:
Set集合就是不允许重复的列表。功能:去重和交集
应用:
抽奖(sadd添加用户,spop随机抽取)
全局去重,比如朋友圈点赞(sadd,srem push:msgId uid,smembers查看所有,scard查看数量)
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。社交中的共同关注(交集)。可能认识的人(差集)。
底层结构:
intset : 元素是64位以内的整数
hashtable:元素是64位以外的整数
相关命令:
SADD key member [member ...] 添加一个或者多个元素到集合(set)里
SCARD key 获取集合里面的元素数量
SDIFF key 在第一个name对应的集合中且不在其他name对应的集合的元素集合
SDIFFSTORE destination key [key ...] 获取第一个name对应的集合中且不在其他name对应的集合,再将其新加入到dest对应的集合中
SINTER key [key ...] 获得两个集合的交集
SINTERSTORE destination key [key ...] 获得两个集合的交集,并存储在一个集合中
SISMEMBER key member 确定一个给定的值是一个集合的成员
SMEMBERS key 获取集合里面的所有key
SMOVE source destination member 移动集合里面的一个key到另一个集合
SPOP key [count] 获取并删除一个集合里面的元素
SRANDMEMBER key [count] 从集合里面随机获取一个元素,用途:抽奖
SREM key member [member ...] 从集合里删除一个或多个元素,不存在的元素会被忽略
SUNION key [key ...] 返回所有给定集合的并集
SUNIONSTORE destination key [key ...] 合并set元素,并将结果存入新的set里面
SSCAN key cursor [MATCH pattern] [COUNT count] 迭代set里面的元素,用于增量迭代分批获取元素,避免内存消耗太大
Zset:
概述:
在集合的基础上,为每元素排序;元素的排序需要根据另外一个值来进行比较,
所以,对于有序集合,每一个元素有两个值,即:值和分数,分数专门用来做排序。
分数一样时按照value来排序
相关命令:
ZADD key score1 member1 [score2 member2] 添加一个或多个成员到有序集合,或者如果它已经存在更新其分数
ZCARD key 得到的有序集合成员的数量
ZCOUNT key min max 计算一个有序集合成员与给定值范围内的分数
ZINCRBY key increment member 在有序集合增加成员的分数
ZINTERSTORE destination numkeys key [key ...] 多重交叉排序集合,并存储生成一个新的键有序集合。
ZLEXCOUNT key min max 计算一个给定的字典范围之间的有序集合成员的数量
ZRANGE key start stop [WITHSCORES] 由索引返回一个成员范围的有序集合(从低到高)
ZRANGEBYLEX key min max [LIMIT offset count]返回一个成员范围的有序集合(由字典范围)
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] 返回有序集key中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员,有序集成员按 score 值递增(从小到大)次序排列
ZRANK key member 确定成员的索引中有序集合
ZREM key member [member ...] 从有序集合中删除一个或多个成员,不存在的成员将被忽略
ZREMRANGEBYLEX key min max 删除所有成员在给定的字典范围之间的有序集合
ZREMRANGEBYRANK key start stop 在给定的索引之内删除所有成员的有序集合
ZREMRANGEBYSCORE key min max 在给定的分数之内删除所有成员的有序集合
ZREVRANGE key start stop [WITHSCORES] 返回一个成员范围的有序集合,通过索引,以分数排序,从高分到低分
ZREVRANGEBYSCORE key max min [WITHSCORES] 返回一个成员范围的有序集合,以socre排序从高到低
ZREVRANK key member 确定一个有序集合成员的索引,以分数排序,从高分到低分
ZSCORE key member 获取给定成员相关联的分数在一个有序集合
ZUNIONSTORE destination numkeys key [key ...] 添加多个集排序,所得排序集合存储在一个新的键
ZSCAN key cursor [MATCH pattern] [COUNT count] 增量迭代排序元素集和相关的分数
底层结构:
ziplist当元素的个数比较少,且元素都是小整数或短字符串时。
skiplist + dict当元素的个数比较多或元素不是小整数或短字符串时,同时拥有 范围操作+快速查找
skiplist:
redis中skiplist的MaxLevel设定为32层
为了提高搜索效率,redis会缓存MaxLevel的值,在每次插入/删除节点后都会去更新这个值,这样每次搜索的时候不需要从32层开始搜索,而是从MaxLevel指定的层数开始搜索
ziplist:
ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。
它能以O(1)的时间复杂度在表的两端提供push和pop操作。
ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。
结构:
<zlbytes><zltail><zllen><entry>...<entry><zlend>
应用:
排行榜问题:
概述:
redis有序集合,zrevrange查看排名
相同分数问题:
redis在遇到分数相同时是按照集合成员自身的字典顺序来排序
考虑在分数中加入时间戳,计算公式为:
带时间戳的分数 = 实际分数*10000000000 + (9999999999 – timestamp)
实际分数则扩大10^10倍,然后把两部分相加的结果作为zset的分数。考虑到要按时间倒序排列,所以时间戳这部分需要颠倒一下,这便是用9999999999减去时间戳的原因。当我们要读取玩家实际分数时,只需去掉后10位即可。
延时任务:
范围查找:
常用命令:
key相关操作:
DEL key [key...] 删除
EXISTS key 判断key是否存在
KEYS pattern 返回符合正则匹配的全部key
KEYS * 匹配数据库中所有 key 。
KEYS h?llo 匹配 hello , hallo 和 hxllo 等。
KEYS h*llo 匹配 hllo 和 heeeeello 等。
KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo
EXPIRE key time 为某个redis的某个name设置超时时间
RENAME key key 对redis的name重命名为
MOVE key db 将redis的某个值移动到指定的db下
RANDOMKEY 随机获取一个redis的name(不删除)
TYPE key 获取name对应值的类型
查看命令帮助:
help @类型名词
清空数据库:
FLUSHALL 清空全部db
FLUSHDB 清空当前数据库中的所有 key。
切换数据库:
SELECT n
内存分析:
DBSIZE 统计当前数据库key的数量
INFO memory/CPU 查看内存/CPU,redis 4.0版本以上可以用命令:MEMORY USAGE key
DEBUG object key 查看某个key的内存占用
查看最大内存:
配置方式:位于配置文件下的maxmemory bytes,默认64位无限制。推荐设置为最大内存的3/4。
命令行方式:config get maxmemory
bitmap位图(依赖string类型):
setbit setbit key offset value设置key在offset处的bit值(只能是0或者1)。
getbit getbit key offset 获得key在offset处的bit值
bitcount bitcount key 获得key的bit位为1的个数
bitpos bitpos key value 返回第一个被设置为bit值的索引值
bitop bitop and[or/xor/not] destkey key[key …]对多个key 进行逻辑运算后存入destkey中
应用场景:
1、用户每月签到,用户id为key , 日期作为偏移量 1表示签到
2、统计活跃用户, 日期为key,用户id为偏移量 1表示活跃
3、查询用户在线状态, 日期为key,用户id为偏移量 1表示在线
geo地理位置类型:
geo是Redis用来处理位置信息的。在Redis3.2中正式使用。主要是利用了Z阶曲线、Base32编码和geohash算法
geoadd geoadd key 经度 纬度 成员名称1 经度1 纬度1 成员名称2 经度2 纬度 2 ...添加地理坐标
geohash geohash key 成员名称1 成员名称2...返回标准的geohash串
geopos geopos key 成员名称1 成员名称2... 返回成员经纬度
geodist geodist key 成员1 成员2 单位计算成员间距离
georadiusby membergeoradiusbymember key 成员 值单位 count 数asc[desc]根据成员查找附近的成员
应用场景:
1、记录地理位置
2、计算距离
3、查找"附近的人"
布隆过滤器:
概述:
有一定的误判率且无法删除元素。
原理:
本质上由一个很长的二进制向量和一系列随机映射函数组成。
使用多个无偏哈希函数对item进行hash运算,得到多个hash值,每个hash值对bit数组取模得到位数组中的位置index,判断所有index位是否都为1。(降低误报率)
安装:
git clone --recursive https://github.com/RedisBloom/RedisBloom.git
cd redisbloom
make
动态加载模块:
redis-cli
MODULE LOAD /"rebloom.so的绝对路径"/redisbloom.so 加载模块
module list 查看redis当前已加载的插件
MODULE UNLOAD 模块名 卸载模块
配置启动加载:
loadmodule /"rebloom.so的绝对路径"/rebloom.so
命令:
BF.RESERVE 创建一个大小为capacity,错误率为error_rate的空的Bloom BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion] [NONSCALING]
参数说明:
error_rate:期望的错误率(False Positive Rate),该值必须介于0和1之间。该值越小,BloomFilter的内存占用量越大,CPU使用率越高。
capacity:布隆过滤器的初始容量,即期望添加到布隆过滤器中的元素的个数。当实际添加的元素个数超过该值时,布隆过滤器将进行自动的扩容,该过程会导致性能有所下降,下降的程度是随着元素个数的指数级增长而线性下降。
expansion:当添加到布隆过滤器中的数据达到初始容量后,布隆过滤器会自动创建一个子过滤器,子过滤器的大小是上一个过滤器大小乘以expansion。expansion的默认值是2,也就是说布隆过滤器扩容默认是2倍扩容。
NONSCALING:设置此项后,当添加到布隆过滤器中的数据达到初始容量后,不会扩容过滤器,并且会抛出异常((error) ERR non scaling filter is full)。
BF.ADD 向key指定的Bloom中添加一个元素item BF.ADD {key} {item}
BF.MADD 向key指定的Bloom中添加多个元素 BF.MADD {key} {item} [item...]
BF.INSERT 向key指定的Bloom中添加多个元素,添加时可以指定大小和错误率,且可以控制在Bloom不存在的时候是否自动创建
BF.INSERT {key} [CAPACITY {cap}] [ERROR {error}] [EXPANSION expansion] [NOCREATE] [NONSCALING] ITEMS {item...}
参数说明:
CAPACITY:初始容量 [如果过滤器已创建,则此参数将被忽略]。
ERROR:容错率[如果过滤器已创建,则此参数将被忽略]。
BF.EXISTS 检查一个元素是否可能存在于key指定的Bloom中 BF.EXISTS {key} {item}
BF.MEXISTS 同时检查多个元素是否可能存在于key指定的Bloom中 BF.MEXISTS {key} {item} [item...]
BF.SCANDUMP 对Bloom进行增量持久化操作 BF.SCANDUMP {key} {iter}
BF.LOADCHUNK 加载SCANDUMP持久化的Bloom数据 BF.LOADCHUNK {key} {iter} {data}
BF.INFO 查询key指定的Bloom的信息 BF.INFO {key}
输出说明:
Capacity:预设容量;
Size:实际占用情况,但如何计算待进一步确认;
Number of filters:过滤器层数;
Number of items inserted:已经实际插入的元素数量;
Expansion rate:子过滤器扩容系数(默认2);
BF.DEBUG 查看BloomFilter的内部详细信息(如每层的元素个数、错误率等)BF.DEBUG {key}
输出说明:
bytes:占用字节数量;
bits:占用bit位数量,bits = bytes * 8;
hashes:该层hash函数数量;
hashwidth:hash函数宽度;
capacity:该层容量(第一层为BloomFilter初始化时设置的容量,第2层容量 = 第一层容量 * expansion,以此类推);
size:该层中已插入的元素数量(各层size之和等于BloomFilter中已插入的元素数量size);
ratio:该层错误率(第一层的错误率 = BloomFilter初始化时设置的错误率 * 0.5,第二层为第一层的0.5倍,以此类推,ratio与expansion无关);
扩容:
扩容触发条件:
实际插入 > 容量
原理:
因为布隆过滤器的不可逆,我们没法重新建一个更大的布隆过滤器然后去把数据重新导入。
保留原有的布隆过滤器,建立一个更大的,新增数据都放在新的布隆过滤器中,去重的时候检查所有的布隆过滤器。
HyperLogLog:
pfadd key element [element...] 加入一个元素
pfcount key [key...] 计算已加入的元素的unique数
pfmerge destkey sourcekey [sourcekey...]合并多个统计的hyperloglog为一个
stream数据流类型:
发布与订阅:
概述:
Redis的发布订阅机制包括三个部分,publisher,subscriber和Channel
发布者和订阅者都是Redis客户端,Channel则为Redis服务器端。
发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
频道/模式的订阅与退订:
订阅
subscribe channel1 channel2 ..
发布消息
publish channel message
退订
unsubscribe ch1
模式匹配
psubscribe ch*
punsubscribe
原理:
typedef struct redisClient {
...
dict *pubsub_channels;
list *pubsub_patterns;
...
} redisClient;
struct redisServer {
...
dict *pubsub_channels;
list *pubsub_patterns;
int notify_keyspace_events;
...
};
当客户端向某个频道发送消息时,Redis首先在redisServer中的pubsub_channels中找出键为该频道的结点,遍历该结点的值,即遍历订阅了该频道的所有客户端,将消息发送给这些客户端。
然后,遍历结构体redisServer中的pubsub_patterns,找出包含该频道的模式的结点,将消息发送给订阅了该模式的客户端。
使用场景:
哨兵模式
哨兵通过发布与订阅的方式与Redis主服务器和Redis从服务器进行通信。
Redisson框架使用
在Redisson分布式锁释放的时候,是使用发布与订阅的方式通知的
事务:
概述:
Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。
Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
Redis不支持回滚操作
事务命令:
multi:用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列
exec:执行命令队列
discard:清除命令队列
watch:监视key,watch+multi实际是一种乐观锁,watch的key变化后mutli的命令全部不执行(先watch,mutli再get,然后set)
unwatch:清除监视key
事务执行:
如果某条命令在入队过程中发生格式编译错误,redisClient将flags置为REDIS_DIRTY_EXEC,EXEC命令将会失败返回。
如果所有命令在入队过程正常,但执行失败(语义运行时错误),前面的命令仍会执行成功。
如果exec执行前,watch的key发生了变化,客户端的flags置为REDIS_DIRTY_CAS,那么这一组命令将会执行失败。
LUA脚本:
概述:
从Redis2.6.0版本开始,通过内置的lua编译/解释器,可以使用EVAL命令对lua脚本进行求值。
脚本的命令是原子的,RedisServer在执行脚本命令中,不允许插入新的命令
EVAL命令:
语法:
EVAL script numkeys key [key ...] arg [arg ...]
命令说明:
script参数:是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数。
numkeys参数:用于指定键名参数的个数。
key [key ...]参数: 从EVAL的第三个参数开始算起,使用了numkeys个键(key),表示在脚本中所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
arg [arg ...]参数:可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似(ARGV[1] 、 ARGV[2] ,诸如此类)。
示例:
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
注意事项:
在脚本中,使用return语句将返回值返回给客户端,如果没有return,则返回nil
调用Redis命令:
redis.call():
返回值就是redis命令执行的返回值
如果出错,则返回错误信息,不继续执行
redis.pcall():
返回值就是redis命令执行的返回值
如果出错,则记录错误信息,继续执行
EVALSHA:
EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。
为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)
SCRIPT命令
SCRIPT FLUSH :清除所有脚本缓存
SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
SCRIPT LOAD :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它
SCRIPT KILL :杀死当前正在运行的脚本
执行:
./redis-cli -h 127.0.0.1 -p 6379
脚本复制:
Redis 传播 Lua 脚本,在使用主从模式和开启AOF持久化的前提下:
当执行lua脚本时,Redis 服务器有两种模式:脚本传播模式和命令传播模式。
脚本传播模式
脚本传播模式是 Redis 复制脚本时默认使用的模式
Redis会将被执行的脚本及其参数复制到 AOF 文件以及从服务器里面。
注意:
在这一模式下执行的脚本不能有时间、内部状态、随机函数(spop)等。执行相同的脚本以及参数必须产生相同的效果。在Redis5,也是处于同一个事务中。
命令传播模式
处于命令传播模式的主服务器会将执行脚本产生的所有写命令用事务包裹起来,然后将事务复制到 AOF文件以及从服务器里面。
因为命令传播模式复制的是写命令而不是脚本本身,所以即使脚本本身包含时间、内部状态、随机函数等,主服务器给所有从服务器复制的写命令仍然是相同的。
示例:
eval "redis.replicate_commands();redis.call('set',KEYS[1],ARGV[1]);redis.call('set',KEYS[2],ARGV[2])" 2 n1 n2 zhaoyun11 zhaoyun22
管道,事务和脚本(lua)三者的区别:
相同点:
三者都可以批量执行命令。mset、管道、事务和LUA Script还是必须遵循所有key在一个分片上的规则要求。
差异点:
mset、hmset命令:
分片场景下不支持拆分。需要客户端拆分。mget仅支持单个slot内批量执行。
只能针对单个key
管道:
无原子性,命令都是独立的,属于无状态的操作。适合执行这种连续,且无相关性的命令。
有序返回结果。
与mset比较:
管道支持各种独立命令,而mget、mset只能支持字符串一批的key。
相同点:mget和mset命令也是为了减少网络连接和传输时间所设置的,其本质和pipeline的应用区别不大
与事务比较:
没有任何事务保证,其他client的命令可能会在本pipeline的中间被执行。
相同点:
只有在事务执行完成时,才会把事务中多个命令的结果一并返回给客户端
事务:
原理:
乐观锁,watch某个key
与管道比较:
先逐批发送,再一次执行(或取消)。管道的话,大量命令会被分为多个包,以包为单位逐批发送到redis服务器执行。
有事务性
lua脚本:
原理:
单线程执行命令
与事务比较:
事务和脚本是有原子性的,其区别在于脚本可借助Lua语言可在服务器端存储的便利性定制和简化操作
脚本的原子性要强于事务,脚本执行期间,另外的客户端 其它任何脚本或者命令都无法执行,脚本的执行时间应该尽量短,不能太耗时的脚本
lua脚本可以获取中间命令的执行结果
相同点:
如果执行期间出现运行错误,之前的执行过的命令是不会回滚的。
使用场景:
需要中间值来编排后面的命令
慢查询日志:
设置:
slowlog-log-slower-than 10000 #执行时间超过多少微秒的命令请求会被记录到日志上 0 :全记录 <0 不记录
slowlog-max-len 128 #slowlog-max-len 存储慢查询日志条数
命令:
slowlog get [n]
slowlog reset
慢查询定位&处理:
1、尽量使用短的key,对于value有些也可精简,能使用int就int。
2、避免使用keys *、hgetall等全量操作。
3、减少大key的存取,打散为小key 100K以上
4、将rdb改为aof模式,rdb fork 子进程 数据量过大 主进程阻塞 redis性能大幅下降关闭持久化 , (适合于数据量较小,有固定数据源)
5、想要一次添加多条数据的时候可以使用管道
6、尽可能地使用哈希存储
7、尽量限制下redis使用的内存大小,这样可以避免redis使用swap分区或者出现OOM错误内存与硬盘的swap
持久化:
RDB:
概述:
在指定的时间间隔内生成数据集的时间点快照
配置:
save ""
save 900 1
save 300 10
save 60 10000
触发时间:
1. 符合自定义配置的快照规则
2. 执行save或者bgsave命令
3. 执行flushall命令
4. 执行主从复制操作 (第一次
执行流程:
1. Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(aof文件重写命令)的子进程,如果在执行则bgsave命令直接返回。
2. 父进程执行fork(调用OS函数复制主进程)操作创建子进程,这个复制过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令。
3. 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令。
4. 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。(RDB始终完整)
5. 子进程发送信号给父进程表示完成,父进程更新统计信息。
6. 父进程fork子进程后,继续工作。
优点:
一个文件保存了 Redis 在某个时间点上的数据集
在恢复大数据集时的速度比 AOF 的恢复速度要快。
RDB是二进制压缩文件,占用空间小,便于传输(传给slaver)
缺点:
不保证数据完整性,会丢失最后一次快照以后更改的所有数据
主进程fork子进程,可以最大化Redis性能,主进程不能太大,Redis的数据量不能太大,复制过程中主进程阻塞
AOF:
概述:
记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。
OF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。
配置:
appendonly yes
dir ./
appendfilename appendonly.aof
原理:
AOF文件中存储的是redis的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:
1.命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
2.缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
3.文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话, fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。
AOF 保存模式:
AOF_FSYNC_NO :不保存。每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行, 但 SAVE 会被略过。
在这种模式下, SAVE 只会在以下任意一种情况中被执行:这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。
Redis 被关闭
AOF 功能被关闭
系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)SAVE 操作是由后台子线程(fork)调用
AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐)SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。
AOF重写:
Redis可以在 AOF体积变得过大时,自动地在后台(Fork子进程)对 AOF进行重写。
子进程在进行 AOF 重写期间, 主进程还需要继续处理命令, 而新的命令可能对现有的数据进行修改
Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用,Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外,
还会追加到这个缓存中。
现有的 AOF 功能会继续执行,即使在 AOF 重写期间发生停机,也不会有任何数据丢失。
触发方式:
1.配置触发:
auto-aof-rewrite-percentage 100表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过,以启动时aof文件大小为准
auto-aof-rewrite-min-size 64mb 限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化
2.执行命令:
bgrewriteaof
优点:
数据丢失较少
AOF存操作命令,采用文本存储(混合)
缺点:
性能较低
混合持久化:
概述:
RDB和AOF各有优缺点,Redis 4.0 开始支持 rdb 和 aof 的混合持久化。如果把混合持久化打开,aof rewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。
RDB的头+AOF的身体---->appendonly.aof
配置:
aof-use-rdb-preamble yes
优点:
用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
应用场景:
内存数据库:rdb + aof 数据不容易丢
有原始数据源:每次启动时都从原始数据源中初始化 ,则 不用开启持久化 (数据量较小)
缓存服务器:rdb 一般 性能高
生产上常用持久化策略:
(1)master关闭持久化
(2)slave开RDB即可,必要的时候AOF和RDB都开启
过期策略:
惰性删除:
惰性删除不再是Redis去主动删除,而是在客户端要获取某个key的时候,Redis会先去检测一下这个key是否已经过期,如果没有过期则返回给客户端,如果已经过期了,那么Redis会删除这个key,不会返回给客户端。
定时过期:
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;
但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
定期删除:
Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
1.从过期字典中随机 20 个 key。
2.删除这 20 个 key 中已经过期的 key;
3.如果过期的 key 比率超过 1/4,那就重复步骤 1;
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
redis采用:
使用key的时候会执行惰性删除 + 定期删除。
遗留问题:
惰性删除 + 定期删除仍然会漏掉很多key,需要依靠内存淘汰机制。
配置:
redis.conf中,hz默认设为10。hz的取值范围是1~500,通常不建议超过100,只有在请求延时非常低的情况下可以将值提升到100。
内存淘汰机制:
expire原理:
typedef struct redisDb {
dict *dict; -- key Value dict 用来维护一个 Redis 数据库中包含的所有 Key-Value 键值对
dict *expires; -- key ttl 用于维护一个 Redis 数据库中设置了失效时间的键(即key与失效时间的映射)。
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
} redisDb;
操作命令:
expire name 2
ttl name
LRU 数据淘汰机制:
概述:
在数据集中随机挑选几个键值对,取出其中 lru 最大的键值对淘汰。
分类:
volatile-lru 从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
allkeys-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
LFU数据淘汰机制:
概述:
LFU (Least frequently used) 最不经常使用
分类:
volatile-lfu
allkeys-lfu
random数据淘汰机制:
分类:
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
其他淘汰机制:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。默认策略
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
如何选取合适的策略?
比较推荐的是两种lru策略。根据自己的业务需求。
如果你使用Redis只是作为缓存,不作为DB持久化,那推荐选择allkeys-lru;如果你使用Redis同时用于缓存和数据持久化,那推荐选择volatile-lru。
allkeys-lru : 在不确定时一般采用策略。 冷热数据交换
volatile-lru : 比allkeys-lru性能差 存 : 过期时间
allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)
自己控制:volatile-ttl 放缓存穿透
持久化如何处理过期?
RDB:
从内存数据库持久化数据到RDB文件:持久化key之前,会检查是否过期,过期的key不进入RDB文件
从RDB文件恢复数据到内存数据库:数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)。
AOF:
从内存数据库持久化数据到AOF文件:当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
AOF重写:重写时,会先判断key是否过期,已过期的key不会重写到aof文件
配置:
maxmemory-policy allkeys-lru
主从复制:
概述:
Redis使用异步复制
复制功能不会阻塞主服务器,也不会阻塞从服务器,只要在 redis.conf 文件中进行了相应的设置
复制功能可以读写分离,主服务器写入
全量同步:
Redis 的全量同步过程主要分三个阶段:
同步快照阶段: Master 创建并发送快照RDB给Slave , Slave 载入并解析快照。Master 同时将此阶段所产生的新的写命令存储到缓冲区。
同步写缓冲阶段: Master 向Slave 同步存储在缓冲区的写操作命令。
同步增量阶段: Master 向Slave 同步写操作命令。
增量同步:
Redis增量同步主要指Slave完成初始化后开始正常工作时, Master 发生的写操作同步到Slave 的过程。
通常情况下, Master 每执行一个写命令就会向Slave 发送相同的写命令,然后Slave 接收并执行。
复制过程:
1)当一个从数据库启动时,会向主数据库发送sync命令。
2)主数据库接收到sync命令后会开始在后台保存快照(执行rdb操作),并将保存期间接收到的命令缓存起来
3)当快照完成后,redis会将快照文件和所有缓存的命令发送给从数据库。
4)从数据库收到后,会载入快照文件并执行收到的缓存的命令。
心跳检测:
在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送命令:
主要作用有三个:
1. 检测主从的连接状态
2. 辅助实现min-slaves
min-slaves-to-write 3 (min-replicas-to-write 3 )
min-slaves-max-lag 10 (min-replicas-max-lag 10)
从服务器的数量少于3个,或者三个从服务器的延迟(lag)值都大于或等于10秒时,主服务器将拒绝执行写命令。这里的延迟值就是上面INFOreplication命令的lag值。
3. 检测命令丢失
主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量,然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,
并将这些数据重新发送给从服务器。
命令:
SLAVEOF 127.0.0.1 6379
config set masterauth root 设置主认证密码
SLAVEOF NO ONE
配置:
replicaof 127.0.0.1 6379
slave-read-only 1 主从配置后从服务器默认只读
masterauth <password>
Sentinel:
概述:
Sentinel(哨兵)是用于监控redis集群中Master状态的工具,是Redis 的高可用性解决方案,sentinel哨兵模式已经被集成在redis2.4之后的版本中。
sentinel是redis高可用的解决方案,sentinel系统可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;
当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。
方案:
一主一从,一主多从
配置:
sentinel monitor mymaster 127.0.0.1 6379 2 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
sentinel down-after-milliseconds mymaster 3000 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
sentinel parallel-syncs mymaster 1 发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步
sentinel failover-timeout mymaster 180000
执行流程:
启动并初始化Sentinel
每个Sentinel会创建2个连向主服务器的网络连接。命令连接:用于向主服务器发送命令,并接收响应;订阅连接:用于订阅主服务器的—sentinel—:hello频道。
获取主服务器信息
Sentinel默认每10s一次,向被监控的主服务器发送info命令,获取主服务器和其下属从服务器的信息。
获取从服务器信息
当Sentinel发现主服务器有新的从服务器出现时,Sentinel还会向从服务器建立命令连接和订阅连接。
在命令连接建立之后,Sentinel还是默认10s一次,向从服务器发送info命令,并记录从服务器的信息。
向主服务器和从服务器发送消息(以订阅的方式)
默认情况下,Sentinel每2s一次,向所有被监视的主服务器和从服务器所订阅的—sentinel—:hello频道上发送消息,消息中会携带Sentinel自身的信息和主服务器的信息。
接收来自主服务器和从服务器的频道信息
当Sentinel与主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接
检测主观下线状态
超时后
检查客观下线状态
如果达到Sentinel配置中的quorum数量的Sentinel实例都判断主服务器为主观下线,则该主服务器就会被判定为客观下线(ODown)。
选举Leader Sentinel:
当一个主服务器被判定为客观下线后,监视这个主服务器的所有Sentinel会通过选举算法(raft),选出一个Leader Sentinel去执行failover(故障转移)操作。
哨兵leader选举
Raft算法
故障转移
1. 过滤掉主观下线的节点
2. 选择slave-priority最高的节点,如果由则返回没有就继续选择
3. 选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整,如果由就返回了,没有就继续
4. 选择run_id最小的节点,因为run_id越小说明重启次数越少
Cluster:
概述:
单机Redis的网络I/O能力和计算资源是有限的,将请求分散到多台机器,充分利用多台机器的计算能力可网络带宽,有助于提高Redis总体的服务能力。
安装:
先启动6个redis server(注意切换路径启动,默认生成文件在当前路径下)
redis-cli --cluster create 具体ip:6374 具体ip:6375 具体ip:6376 具体ip:6377 具体ip:6378 具体ip:6379 --cluster-replicas 1
注意不能填127.0.0.1,否则cluster客户端相互连接的时候出错。
连接:
redis-cli -c -p
写入的数据按key的hash值分配结点,redis-cli会自动跳转,获取时也会自动跳转相应的服务器。
cli内通过cluster nodes查看节点信息
或者:
redis-cli --cluster check 127.0.0.1:6374
moved重定向:
如果保存数据的槽不在当前节点的管理范围内,则向客户端返回moved重定向异常
ask重定向:
如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁移到别的节点了,就会返回ask,这就是ask重定向机制
新增节点:
命令:
redis-server /etc/redis/redis6373.conf
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 第一个为新增节点,第二个为集群中存在的随机节点
注意:新增的节点成为master库,有0个slots,没有从属的slave。登陆客户端cluster nodes查看即可。
重新分配slots给新节点即可,可以单个节点,或者全部节点平均分配。
分配slots
redis-cli --cluster reshard 127.0.0.1:7000
选择要重新分配的slot数量
然后输入目标的ID,可以通过redis-cli -p 7000 cluster nodes | grep myself查看,再输入源ID,可以多个,这样数量会平分
方式二:
redis-cli reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes
新增从节点:
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave 作为备库,随机选取少从库的主库作为master
指定master
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e
# 需注意新节点以原来的dump文件为库中,有没有key
删除节点:
redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>`
注:移除master库需要为空。通过reshard或者kill自动变为从库即可
从库变更:
CLUSTER REPLICATE <master-node-id>
单点故障:
手动kill掉一个master后,会自动从slave中选举新的master。当故障修复后,自动加入变为slave。
修复:
redis-cli --cluster fix 127.0.0.1:7006
卸载集群:
kill掉所有进程
删除pid文件、node文件以及appendonly.aof、dump.rdb文件
集群架构方案:
1.Replication+Sentinel(单点写)
利用Sentinel做master和slave的主从切换。
Sentinel的作用有三个:
监控:Sentinel 会不断的检查主服务器和从服务器是否正常运行。
通知:当被监控的某个redis服务器出现问题,Sentinel通过API脚本向管理员或者其他的应用程序发送通知。
自动故障转移:当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系 的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点。
切换过程:
当Master宕机的时候,Sentinel会选举出新的Master,并根据Sentinel中client-reconfig-script脚本配置的内容,去动态修改VIP(虚拟IP),将VIP(虚拟IP)指向新的Master。
缺点:
(1)主从切换的过程中会丢数据
(2)Redis只能单点写,不能水平扩容
2.Redis Cluster(按节点分片)
优点:
(1)无需Sentinel哨兵监控,如果Master挂了,Redis Cluster内部自动将Slave切换Master
(2)可以进行水平扩容
(3)支持自动化迁移,当出现某个Slave宕机了,那么就只有Master了,这时候的高可用性就无法很好的保证了,万一master也宕机了,咋办呢?
针对这种情况,如果说其他Master有多余的Slave ,集群自动把多余的Slave迁移到没有Slave的Master 中。
缺点:
(1)批量操作是个坑
不同的key会划分到不同的slot中,因此直接使用mset或者mget等操作是行不通的。报“(error) CROSSSLOT Keys in request don’t hash to the same slot”
如果执行的key数量比较少,就不用mget了,就用串行get操作。
如果真的需要执行的key很多,就使用Hashtag保证这些key映射到同一台redis节点上。
对于key为{foo}.student1、{foo}.student2,{foo}student3,这类key一定是在同一个redis节点上。因为key中“{}”之间的字符串就是当前key的hash tags,
只有key中{ }中的部分才被用来做hash,因此计算出来的redis节点一定是同一个!
(2)资源隔离性较差,容易出现相互影响的情况。
分布式锁:
1.setNx方法
示例:
setNx resourceName value
set resourceName value ex 5 nx # 加入超时时间,防止机器宕机
JAVA示例:
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);其中的requestId为uuid+threadID,保证唯一性
Long result = jedis.setnx(lockKey, requestId); 并发会产生问题,因为没有设置过期时间,jedis.expire(lockKey, expireTime);
redisTemplate.delete(key);
常见问题:
1.setnx的key需要设置过期时间,防止程序突然推出,delete key逻辑执行不到。
2.setnx和expire这两个操作需要保证原子性。
3.删除key的时候,需要判断占有key的是当前线程(key为uuid,uuid可以避免同一个线程切换不同协程的问题)。避免get到锁后,刚好失效,别人又set了,导致del了别人的锁。
if redis.call('get', KEYS[1]) == ARGV[1]
4.释放锁的时候,需要原子性操作:
lua脚本:
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey),Collections.singletonList(requestId));
redis事务也可以,watch + multi + exec:
redisTemplate.watch(key);
if(redisTemplate.opsForValue().get(key).equalsIgnoreCase(value)){
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.mulit();
redisTemplate.delete(key);
redisTemplate.exec();
}
redisTemplate.unwatch();
5.主从复制时主挂了,从还没有复制到,第二个客户端可能获取到锁。redis是AP模式,无法保证C强一致性。
解决是用zookeeper持久顺序节点(CP模型,有时候服务不可用,需要重新请求)
或者是redLock,利用redis cluster,在超过半数以上的主节点获取锁成功,才算成功。
6.无法续租,超过expireTime后,不能继续使用,解决是redission的watchdog
确保过期时间大于业务时间。
2.Redission
概述:
Javaer都知道Jedis,Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redission也是Redis的客户端,相比于Jedis功能简单。
Jedis简单使用阻塞的I/O和redis交互,Redission通过Netty支持非阻塞I/O。
Jedis最新版本2.9.0是2016年的快3年了没有更新,而Redission最新版本是2018.10月更新。
其他版本redis:
spring有spring-data-redis,private RedisTemplate<Serializable, Serializable> rt;
springboot有spring-boot-starter-data-redis
flink用jedis + pipeline
优点:
续租,延迟自动释放,支持分布式
缺点:
宕机恢复不全,AP
加锁原理:
lua脚本
"if (redis.call('exists',KEYS[1])==0) then "+
"redis.call('hset',KEYS[1],ARGV[2],1) ; "+
"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+
"redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+
"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"return redis.call('pttl',KEYS[1]) ;"
JAVA示例:
RedissonClient redis = Redisson.create();
RLock rLock = redis.getLock("resourcename");
rLock.lock();
rLock.tryLock(5,10,TimeUnit.SECONDS);
RFuture<Boolean> rFuture = rLock.tryLockAsync(5,10,TimeUnit.SECONDS);
fFuture.whenCompleteAsync((result,throwable)->{
System.out.println(result+throwable);
});
解锁:
if(rLock.isLocked()){
if(rLock.isHeldByCurrentThread()){
rLock.unlock();
}
}
3.RedLock
概述:
当机器A申请到一把锁之后,如果Redis主宕机了,这个时候从机并没有同步到这一把锁,那么机器B再次申请的时候就会再次申请到这把锁,
为了解决这个问题Redis作者提出了RedLock红锁的算法,在Redission中也对RedLock进行了实现。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1,lock2,lock3);
lock.lock();
lock.unlock();
原理:
可以看见RedLock基本原理是利用多个Redis集群,用多数的集群加锁成功,减少Redis某个集群出故障,造成分布式锁出现问题的概率。
优点:
对于Redis实现简单,性能对比ZK和Mysql较好。如果不需要特别复杂的要求,那么自己就可以利用setNx进行实现,
如果自己需要复杂的需求的话那么可以利用或者借鉴Redission。对于一些要求比较严格的场景来说的话可以使用RedLock。
缺点:
需要维护Redis集群,如果要实现RedLock那么需要维护更多的集群。
锁的其他实现:
1.mysql
适用场景: Mysql分布式锁一般适用于资源不存在数据库,如果数据库存在比如订单,那么可以直接对这条数据加行锁,不需要我们上面多的繁琐的步骤,
比如一个订单,那么我们可以用select * from order_table where id = 'xxx' for update进行加行锁,那么其他的事务就不能对其进行修改。
优点:理解起来简单,不需要维护额外的第三方中间件(比如Redis,Zk)。
缺点:虽然容易理解但是实现起来较为繁琐,需要自己考虑锁超时,加事务等等。性能局限于数据库,一般对比缓存来说性能较低。
对于高并发的场景并不是很适合。
2.ZooKeeper
临时节点
CP模型,有时候请求会不可用,需要重新请求
优点:比redis可靠,可以实现读写锁,而redis不行
缺点:ZK需要额外维护,增加维护成本,性能和Mysql相差不大,依然比较差。并且需要开发人员了解ZK是什么。
适合几台的集群规模
3.Redis小结
优点:对于Redis实现简单,性能对比ZK和Mysql较好。如果不需要特别复杂的要求,那么自己就可以利用setNx进行实现,如果自己需要复杂的需求的话那么可以利用或者借鉴Redission。
对于一些要求比较严格的场景来说的话可以使用RedLock。
可以大规模部署,适合高并发
缺点:需要维护Redis集群,如果要实现RedLock那么需要维护更多的集群。
利用Watch实现Redis乐观锁:
流程:
1、利用redis的watch功能,监控这个redisKey的状态值
2、获取redisKey的值
3、创建redis事务
4、给这个key的值+1
5、然后去执行这个事务,如果key的值被修改过则回滚,key不加1
示例:
jedis1.watch(redisKey);
String redisValue = jedis1.get(redisKey);
int valInteger = Integer.valueOf(redisValue);
if (valInteger < 20) {
Transaction tx = jedis1.multi();
tx.incr(redisKey);
List list = tx.exec();
if (list != null && list.size() > 0) {
System.out.println("用户:" + userInfo + ",秒杀成功!当前成功人数:" + (valInteger + 1));
}
}
抗高并发:
分段锁
合并扣减
缓存问题:
缓存穿透:
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决:
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
缓存,如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
互斥锁
缓存雪崩
在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:
1.大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。
2.讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3.双缓存,缓存A的失效时间为20分钟,缓存B不设失效时间。
缓存击穿:
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决:
1.使用互斥锁(mutex key)
在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降
2.热点数据缓存永远不过期。
缓存预热:
缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
缓存预热解决方案:
(1)数据量不大的时候,工程启动的时候进行加载缓存动作;
(2)数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
(3)数据量太大的时候,优先保证热点数据进行提前加载到缓存。
缓存降级:
缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
在项目实战中通常会将部分热点数据缓存到服务的内存中,这样一旦缓存出现异常,可以直接使用服务的内存数据,从而避免数据库遭受巨大压力。
服务熔断属于降级方式的一种!
熔断
Hystrix(更高可以用Sentinel)
如何使用hystrix熔断呢,总的来说分为4个步骤:
第一步:定义你调用的外部系统的服务
第二步:设置回调函数(当超时或者熔断了会调用回调函数)
第三步:使用hystrix的api调用第一步定义好的服务
第四步:获取最终结果(结果可能时正确的,也可能是一个err)
Istio
istio可以在不更改应用程序代码的情况下配置和使用。
Hystrix的使用需要更改每个服务来引入Hystrix libraries。
Istio提高了网格中服务的可靠性和可用性。但是,应用程序需要处理错误并有一定的fall back行为。例如当负载平衡池中的所有服务实例都出现异常时,Envoy将返回HTTP 503。
当上游服务返回 HTTP 503 错误,则应用程序需要采取回退逻辑。与此同时,Hystrix也提供了可靠的fall back实现。它允许拥有所有不同类型的fall backs:单一的默认值、缓存或者去调用其他服务。
配置istio的envoy
resilience4j:
比hystrix更轻量
限流
Istio无需对代码进行任何更改就可以为应用增加熔断和限流功能。Istio中熔断和限流在DestinationRule的CRD资源的TrafficPolicy中设置,
一般设置连接池(ConnectionPool)限流方式和异常检测(outlierDetection)熔断方式。两者ConnectionPool和outlierDetection各自配置部分参数,
其中参数有可能存在对方的功能,并没有很严格的区分出来,如主要进行限流设置的ConnectionPool中的maxPendingRequests参数,最大等待请求数,如果超过则也会暂时的熔断。
编码结构:
字符串对象:
概述:
SDS(Simple Dynamic String),用于存储字符串和整型数据。
代码:
struct sdshdr{
int len;
int free;
char buf[];
}
场景:
存储字符串和整型数据、存储key、AOF缓冲区和用户输入缓冲。
跳跃表:
概述:
将有序链表中的部分节点分层,每一层都是一个有序链表。
查找:
在查找时优先从最高层开始向后查找,当到达某个节点时,如果next节点值大于要查找的值或next指针指向null,则从当前节点下降一层继续向后查找。
插入:
遍历各级索引,找到插入节点的前驱节点,将节点插入进最底层链表 O(1)
理想的跳跃表,上层比下层少一倍。
实现1:通过抛硬币(概率1/2)的方式来决定新插入结点跨越的层数,每上升一层,修改前后节点
实现2:通过随机数,如果层级高于当前层级,则更新 currentLevel,逐层修改节点
通过抛硬币的方式来决定是否需要进行提升,如果为正则提升,并继续抛硬币,为反面则停止 O如果提升时已处于最高层,则再创建一层(logN)
删除:
找到指定元素并删除每层的该元素即可
代码:
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
优势:
1、可以快速查找到需要的节点
2、可以在O(1)的时间复杂度下,快速获得跳跃表的头节点、尾结点、长度和高度。
场景:
有序集合的实现
字典:
概述:
字典dict又称散列表(hash),是用来存储键值对的一种数据结构。
底层结构:
数组
用来存储数据的容器,采用头指针+偏移量的方式能够以O(1)的时间复杂度定位到数据所在的内存地址。
Hash函数 :
数组下标=hash(key)%数组容量(hash值%数组容量得到的余数)
Hash冲突:
采用单链表在相同的下标位置处存储原始key和value
代码:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx;
int iterators;
} dict;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
字典扩容:
1. 初次申请默认容量为4个dictEntry,非初次申请为当前hash表容量的一倍。
2. rehashidx!=-1表示要进行rehash操作。
3. 新增加的数据在新的hash表h[1]
4. 修改、删除、查询在老hash表h[0]、新hash表h[1]中(rehash中)
5. 将老的hash表h[0]的数据重新计算索引值后全部迁移到新的hash表h[1]中,这个过程称为rehash。
渐进式rehash
每次add操作时,判断是否正在ReHash,如果需要则调用_dictRehashStep,每次ReHash一条数据,直到完成整个ReHash
场景:
Redis字典除了主数据库的K-V数据存储以外,还可以用于:散列表对象、哨兵模式中的主从节点管理等在不同的应用中
字典的形态都可能不同,dictType是为了实现各种形态的字典而抽象出来的操作函数(多态)。
压缩列表:
概述:
压缩列表(ziplist)是由一系列特殊编码的连续内存块组成的顺序型数据结构
应用场景:
zset和hash元素个数少且是小整数或短字符串(直接使用)
list用快速链表(quicklist)数据结构存储,而快速链表是双向列表与压缩列表的组合。(间接使用)
整数集合:
概述:
整数集合(intset)是一个有序的(整数升序)、存储整数的连续存储结构。
代码:
typedef struct intset{
uint32_t encoding;
uint32_t length;
int8_t contents[];
}intset;
应用场景:
当Redis集合类型的元素都是整数并且都处在64位有符号整数范围内(2^64),使用该结构体存储。
快速列表:
概述:
quicklist是一个双向链表,链表中的每个节点时一个ziplist结构。quicklist中的每个节点ziplist都能够存储多个数据元素。
代码:
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count;
unsigned int len;
int fill : 16;
unsigned int compress : 16;
} quicklist;
场景:
列表(List)的底层实现、发布与订阅、慢查询、监视器等功能。
redis的热key问题如何解决?
怎么发现热key:
方法一:凭借业务经验,进行预估哪些是热key
其实这个方法还是挺有可行性的。比如某商品在做秒杀,那这个商品的key就可以判断出是热key。缺点很明显,并非所有业务都能预估出哪些key是热key。
方法二:在客户端进行收集
这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。缺点就是对客户端代码造成入侵。
方法三:在Proxy层做收集
有些集群架构是下面这样的,Proxy可以是Twemproxy,是统一的入口。可以在Proxy层做收集上报,但是缺点很明显,并非所有的redis集群架构都有proxy。
方法四:用redis自带命令
方法五:自己抓包评估
利用基于大数据领域的流式计算技术来进行实时数据访问次数的统计,比如 Storm、SparkStreaming、Flink,这些技术都是可以的。发现热点数据后可以写到zookeeper中
如何解决:
(1)利用二级缓存
比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中。
针对这种热key请求,会直接从jvm中取,而不会走到redis层。
(2)备份热key
这个方案也很简单。不要让key走到同一台redis上不就行了。我们把这个key,在多个redis上都存一份不就好了。
接下来,有热key请求进来的时候,我们就在有备份的redis上随机选取一台,进行访问取值,返回数据。
3、利用对热点数据访问的限流熔断保护措施
每个系统实例每秒最多请求缓存集群读操作不超过 400 次,一超过就可以熔断掉,不让请求缓存集群,直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。
(首页不行,系统友好性差)通过系统层自己直接加限流熔断保护措施,可以很好的保护后面的缓存集群。
Big Key
概述:
大key指的是存储的值(Value)非常大
常见场景:
热门话题下的讨论
大V的粉丝列表
序列化后的图片
没有及时处理的垃圾数据
大key的影响:
大key会大量占用内存,在集群中无法均衡(流量都打到这个节点上)
Redis的性能下降,主从复制异常
在主动删除或过期删除时会操作时间过长而引起服务阻塞
如何发现大key:
1、redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。但如果Redis 的key比较多,执行该命令会比较慢
大key的处理:
优化big key的原则就是string减少字符串长度,list、hash、set、zset等减少成员数。
1、string类型的big key,尽量不要存入Redis中,可以使用文档型数据库MongoDB或缓存到CDN上。如果必须用Redis存储,最好单独存储,不要和其他的key一起存储。采用一主一从或多从。
2、单个简单的key存储的value很大,可以尝试将对象分拆成几个key-value, 使用mget获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对redis的IO影响。
3、hash, set,zset,list 中存储过多的元素,可以将这些元素分拆。(常见)
3、删除大key时不要使用del,因为del是阻塞命令,删除时会影响性能。
4、使用unlink命令来实现懒delete
在另一个线程中回收内存,因此它是非阻塞的。
宽表和高表:
大key相当于宽表
尽量多用高表,redis取单个key都是O(1)的时间复杂度
秒杀场景:
1.层层缓存
2.削峰
1)浏览器层请求拦截
a)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求
b)JS层面,限制用户在x秒之内只能提交一次请求 如此限流,80%流量已拦。
2)站点层请求拦截与页面缓存
a)同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面
b)同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面
c)根据营销目的过滤,比如比例通过会员和非会员
d)流量仍然超过预期时,可以随机过滤
e)兜底限流
3)服务层请求拦截与数据缓存
a) 思路1:db操作+悲观锁
对于写请求,做请求队列,每次只透有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”
适合中等热度的扣减场景。
思路2:限流(熔断)优化api接口(nginx限流),db操作+悲观锁
限流可以用组件,也可以本地变量
思路3:
redis缓存事务操作,速度更快。
适合分库的情况。不然多个库事务保证不了。
异步保存到数据库。不能实现最终一致性。适合非交易类的扣减业务。
思路4:
先开启事务,更新缓存,再insert记录库(顺序写详细记录,替换方法是MQ),回滚时redis会归还失败可能少卖
同步worker将记录转为业务数据,业务库通过binlog可以堆缓存进行对账校准。
同步worker可以通过一致性hash分配任务
记录库是无状态存储,和业务无关,可以水平扩容,hash一致性分配
缓存对于热点可以本地,或者多个redis服务,或者一个redis的多个分片
b)对于读请求,cache抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的
如何更新缓存?通过从库+binlog写入。
select for update
xxxx
update stock set count=count-1 where id=xxx and count>0
在对于数据一致性要求非常高的场景中,一般用悲观锁;而乐观锁在version变动频繁的情况下则不适用,比如这里的秒杀系统就不太适合用乐观锁,因为库存变化太快了。
reids+lua类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。
限流&降级&熔断&隔离
非关键路径进行降级,如评论等
抢票系统
多场次
微信抢红包
关键点:
缓存原子减红包数量,然后存到数据库(不保证强一致性)
其他思路:
然后把发送消息发给消息队列,消息队列有大小限制。