Redis

参考

因为这篇文章供个人日常总结之用 历经时间较长 很多参考过的文章不容易寻找 如有引用 请见谅并联系本人添加

  • 七种方案!探讨Redis分布式锁的正确使用姿势
  • Redis中文官网

简介

Remote Dictionary Server - 远程字典服务 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库 并提供多种语言的API

官方介绍

Redis 是一个开源(BSD许可)的内存中的数据结构存储系统 它可以用作数据库、缓存和消息中间件

它支持多种类型的数据结构 如 字符串(strings) 散列(hashes) 列表(lists) 集合(sets)有序集合(sorted sets) 与范围查询 、 bitmaps 、hyperloglogs 和 地理空间(geospatial) 索引半径查询

Redis 内置了 复制(replication)、LUA脚本(Lua scripting)、 LRU驱动事件(LRU eviction)、事务(transactions) 和不同级别的 磁盘持久化(persistence)、并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)

本文组织结构根据官方的介绍来组织

特点

  • 和Memcached相比 它支持存储的value类型相对更多 包括string、list、set、zset(sorted set)和hash

  • 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作 而且这些操作都是原子性的

  • redis支持各种不同方式的排序

  • 与memcached一样 为了保证效率 数据都是缓存在内存中 区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件(持久化) 并且在此基础上实现了master-slave(主从)同步 (集群)数据可以从主服务器向任意数量的从服务器上同步 从服务器可以是关联其他从服务器的主服务器 这使得Redis可执行单层树复制

  • 由于完全实现了发布/订阅机制 使得从数据库在任何地方同步树时 可订阅一个频道并接收主服务器完整的消息发布记录 同步对读取操作的可扩展性和数据冗余很有帮助

使用场景

  • 内存存储 持久化
  • 效率高 -> 高速缓存
  • 发布 订阅
  • 地图信息分析
  • 计时器、计数器

尽量使用Linux 默认端口6379

安装

(官网)

先安装gcc依赖

$ wget https://download.redis.io/releases/redis-6.2.5.tar.gz
$ tar xzf redis-6.2.5.tar.gz
$ cd redis-6.2.5
$ make$ make install # 安装到usr/local/bin下    # The binaries that are now compiled are available in the src directory. Run Redis with:
$ src/redis-server$ src/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"

主要结构

启动关闭和简单使用

mac上测试的

关闭连接 在客户端 使用shutdown

通过指定配置文件启动

为了以后安全和便于操作(服务端作为服务在后台运行)

复制一份redis.conf文件到/usr/local/bin/rconfig 基于这个环境来做

然后修改该配置文件 将该配置修改为yes 以后台运行 (需要停止的话 在客户端shutdown命令

以后启动就用

$ redis-server rconfig/redis.conf # 指定的配置文件启动服务端
$ redis-cli -p 6379 # 访问指定端口号的服务端

远程连接服务器

  • 首先防火墙要开6379端口

  • redis的配置文件redis.conf 将配置文件中的bind 127.0.0.1注释掉 然后重启

  • 修改配置文件中protected-mode配置项为no
    配置文件中protected-mode配置项默认开启yes redis处于保护模式状态 会拒绝来自其它主机的连接

帮助命令

"help @<group>" to get a list of commands in <group>
"help <command>" for help on <command>
"help <tab>" to get a list of possible help topics
"quit" to exit

性能测试

使用redis-benchmark 测试

可选参数

与Memcache区别

两者都是非关系型内存键值数据库 主要有以下不同

  • 数据类型
    Memcached 仅支持字符串类型 而 Redis 支持五种不同的数据类型

  • 数据持久化
    Memcached 不支持持久化 Redis 支持两种持久化策略

  • 分布式
    Memcached 不支持分布式 只能通过在客户端使用一致性哈希来实现分布式存储 这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点

    Redis Cluster 实现了分布式的支持

  • 内存管理机制
    在 Redis 中 并不是所有数据都一直存储在内存中 可以将一些很久没用的 value 交换到磁盘 而 Memcached 的数据则会一直在内存中 Memcached 将内存分割成特定长度的块 存在内存碎片问题

Bitmap

作用

大量数据的快速排序、查找、去重

我个人感觉好像只能用于数值 如果用于其他数据如string 那没有一种天然的下标和数值恒等映射 但如果使用map去映射那么也失去了节约内存的意义

基本思想就是用一个bit位来标记某个元素对应的Value 而Key即是该元素

比如存20亿个数

​ -> 若用int存 (2000000000*4/1024/1024/1024)≈7.45G

​ -> 用bitmap存 20亿位 每一位代表一个数 (2000000000/8/1024/1024/1024)≈0.233G (真正占的内存 并不是实际只申请的内存)

那么如何表示一个数?

一个int4个字节 32位 可以表示32个数是否存在 设要存的最大数为N 那么只需要实际申请N / 32 + 1 个 int 的存储

第一个int 存 0 ~ 31 第二个int 32 ~ 63 …

那么给定整数M 应该处于第 M / 32 个int 在具体的int中处于第 M % 32 位 (从0开始)

  • 添加一个数

    int[] bitmap = new int[N / 32 + 1];int r = M / 32; // 该处于的int索引
    int c = M % 32; // int中该处于具体第几位bitmap[r] |= (1 << c);  // 通过位运算将对应位置1
    
  • 清除一个数

    bitmap[r] &= (~(1 << c)) //
    
  • 查找一个数是否存在

    return bitmap[r] &= (1 << c)
    

应用

  • 快速排序

    假设对0-7内的5个元素(4, 7, 2, 5, 3) 排序(假设这些元素没有重复)我们就可以采用Bit-map的方法来达到排序的目要表示8个数 我们就只需要8个Bit 将这些空间的所有Bit位都置为0 然后将对应位置为1 最后遍历一遍Bit 将为1的位的下标输出 就达到了排序的目的 时间复杂度O(n)

    优点

    • 节约内存、运算效率高

    缺点

    • 申请的内存取决于最大最小数(即实际上是一种映射 但必须做到所有数按照一个规律映射)所以只有当数据比较密集时才有优势
    • 数据不能重复
  • 快速去重

    需求: 20亿个整数中找出不重复的整数的个数 内存不足以容纳这20亿个整数

    根据内存空间不足 -> Bit-map 关键怎么设计我们的Bit-map来表示这20亿个数字的状态

    一个数字的状态只有三种 不存在、只有一个、多个(重复)

    则2bits就足够对一个数字的状态进行存储了 设不存在为00 、存在一次01、存在两次及其以上为11 则大致需要存储空间2G(我个人算出来是0.46g 这个人是不是算了)左右

    然后把这20亿个数字放进去

    • 如果对应的状态位为00 则将其变为01 表示存在一次

    • 如果对应的状态位为01 则将其变为11 即出现多次

    • 如果为11 则对应的状态位保持不变 仍表示出现多次

    最后统计状态位为01的个数 就得到了不重复的数字个数 时间复杂度为O(n)

    实现上就是两个之前的int数组即可

  • 快速查找

    即前面所说的位运算判断是否存在即可

总结Bitmap的主要应用场合:表示连续(较密集)的关键字序列的状态(状态数/关键字个数 越小越好)

布隆过滤器

Bloom Filter 是一个基于概率的数据结构 用来高效插入和查询(某个元素是否在集合内) 特点是快速、内存占用小

高效的代价是基于概率 它只能明确一个元素绝对不在集合内或可能在集合内 其基础数据结构是一个Bit向量(数组)

主要应用于大规模数据下不需要精确过滤的场景 如检查垃圾邮件地址、爬虫URL地址去重、解决缓存穿透问题

想判断一个元素是不是在一个集合里一般想到的是将集合中所有元素保存起来 然后通过比较确定

链表、树、散列表(哈希表)等等数据结构都是这种思路 但是随着集合中元素的增加 需要的存储空间越来越大 同时检索速度也越来越慢 检索时间复杂度分别是O(n)、O(log n)、O(1)

而布隆过滤器的原理是

当一个元素被加入集合时 通过 K 个散列函数将这个元素映射成一个位数组(Bit array)中的 K 个点 把它们置为 1

  • 检索时 只要看看这些点是不是都是1就知道元素是否在集合中如果这些点有任何一个 0 则被检元素一定不在
  • 如果都是1 则被检元素很可能

流程

  1. 首先需要 k 个 hash 函数 每个函数可以把 key 散列成为 1 个整数
  2. 初始化时 需要一个长度为 n 比特的数组 每个比特位初始化为 0
  3. 某个 key 加入集合时 用 k 个 hash 函数计算出 k 个散列值 并把数组中对应的比特位置为 1
  4. 判断某个 key 是否在集合时 用 k 个 hash 函数计算出 k 个散列值 并查询数组中对应的比特位 如果所有的比特位都是1 可以认为在集合中(接受范围内) 若有一个不是1 那么判定绝对不在集合中

可能存在的原因是 其他数的存在也可能造成其中某些位为1 同时造成无法逆向删除某个值 当数越来越多 很快向量就会bit位均为1 查询任何数都是可能存在 达不到过滤的效果了 所以要仔细考虑布隆过滤器长度m(内存和准确率的权衡)和哈希函数个数k(效率的考量 太多则容易让布隆过滤器置1速度更快 太少 误报率上升)其适合的(推导略去)

可用于缓解Redis缓存穿透问题 请求到达 缓存中有直接返回 若无 通过布隆过滤器判断 若绝对不存在于原数据库 直接返回 若可能存在 才去访问源数据库 降低访问频率

五大数据类型

数据类型 可以存储的值 操作
STRING 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作
LIST 列表 从两端压入或者弹出元素 对单个或者多个元素进行修剪 只保留一个范围内的元素
SET 无序集合 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在
ZSET 有序集合 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名

Redis-key(重要)

  • Redis有16个数据库(conf文件中定义了) -> 用select n 命令切换

  • 查看数据库大小 -> dbsize

  • 查看数据库存了些什么key(上述五种类型) keys *

  • 查看key的类型 type key

  • 查看数据库是否存了某个键 exists key命令

  • 移动某个键到另一个数据库 move key database_number

  • 移除某个键 del key

  • 设置键过期时间 expire key time (过期后就不能获取了 通过ttl可以查询剩余时间

  • 清空当前数据库 flushdb

  • 清空所有数据库 flushall

理解

  • 区别在于String 是一个key对应一个value

  • List、Set、Hash、ZSet等是一个key对应一个数据结构 相当于多个value 然后不同结构的性质不同

相当于本质上Redis外层有一个map 存了这些key 分别对应不同的数据结构 比如说key - map

String

操作

# 实际上就是字符串的一些方法
set key1 v1
get key1 del key1 # 因为一个键只对应一个值 那么删除值就直接删除键strlen key1 # 长度
append key1 "test" # 附加 不存在时相当于set
getrange key1 0 3  # 截取字符串
setrange key1 1 "te" # 替换字符串 指定开始 自动判断长度# 数值可以按数值操作
set views 0
incr views  # 自增
decr views
incrby views 10 # 指定步长
decrby views 10# set with expire
setex key seconds value # 带过期时间的设置key
# set if not exist  重要
setnx key value # 若不存在该key才设置 在分布式锁中常用 是原子性操作# 批量设置 获取
mset key1 v1 key2 v2 ...
mget kye1 key2 ...
msetnx key1 v1 key2 v2 ... # 需要注意的是 是原子性操作# 对象 一般可以通过 set user:1 {json字符串} 来存取
# 也可以将user:1:name等整体作为key 存取# 复合操作
getset key value # 先获取再设置

String使用场景

  • 计数器
  • 统计多单位数量
  • 粉丝数
  • 对象缓存存储

List

列表

# 判断存在
exists key(e.g mylist) # 添加元素
lpush/rpush key elements  # 插入到左边或右边 可多个
linsert key BEFORE|AFTER pivot element # 在指定元素(第一个)的前后插入 # 获取元素
llen key # 总长度
lrange tls 0 2   # 获取某个范围的元素
lindex key index # 获取指定下标的元素# 删除元素
lpop/rpop key [count]# 默认移除左右的第一个元素 可指定个数
lrem key num element  # list可能重复 按顺序移除指定个数的元素
ltrim start end  # 从左开始截取某个范围 # 更新元素
lset key index element # 根据下标更新元素 不存在不成功rpoplpush src dest  # 将src最右元素移动到dest最左

本质是双向链表 但是带了下标 实现是使用了个头尾节点保存在一个结构里

但发现没有查询某个指定元素是否存在的功能

可以用于实现栈(Lpush Lpop 先进后出) 、消息队列 (Lpush Rpop 先进先出)

Set

集合 主要特点是不能重复

# 添加元素
sadd setkey elements # 可多个# 获取元素
scard key # 获取元素个数
smembers key  # 获取名为key的set的所有元素
srandmember key # 获取set中随机一个元素# 判断某个元素是否在指定set中
sismember key element# 删除元素
srem key element # 删除指定元素 可多个
spop key # 随机删除一个元素 我个人感觉不是随机的 # 更新元素 无# 移动元素 (set间
smove src dest element # 将src set的element移到dest set 可多个# 集合的并交差
sunion set1 set2 ...# 并
sinter set1 set2 ...# 交
sdiff  set1 set2 ...# 差sunionstore storeset set1 set2 ... # 将结果存到指定集合

好像又没更新指定元素(只能通过删除再添加?)

可以实现如共同关注等功能

Hash

redis一个key对应一个Map集合 跟String的操作类似 可以将String理解为 key - string - stringvalue 一个键值对 且k=v

# 添加或更新元素(存在的情况下)
hset key(hash) field value
hmset key filed1 value1 filed2 value 2 ...  # 批量hsetnx key field value # 类比即可 # 获取
hget key filed  # 指定字段
hmget key filed1 filed2 ... # 批量hlen key # 长度
hgetall key # 获取key对应的map的所有键值对
hkeys key # 获取map的所有字段
hvals key # 获取map的所有字段值# 删除
hdel key filed1 ...  # 删除指定的字段 # 数值
hincyby key filed number # 自增 + number
hdecyby key filed number # # 判断指定字段是否存在
HEXISTS key field# 字段数量
HLEN key

适合对象的存储 key设为对象 map中放对象的属性和值

ZSet

有序集合 在set的基础上增加一个值(score)用于排序

# 添加元素
ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]# 排序  默认升序 min max  降序时 max min rev
ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
ZRANGEBYSCORE/BYLEX
# 降序也可以使用
ZREVRANGE key start stop [WITHSCORES]# 获取
zcard key # 获取元素个数
zcount key min max # 获取指定区间的元素个数# 删除
zrem key elements # 可多个
# set有的方法都可以类比看有无 大部分都有 只不过因为score多了很多其他操作

可以用于一些需要排序的场合 班级成绩表、工资表排序 还可以以权重形式 反正就是多一个score 可以直接使用为数据 也可以用作数据的一些辅助 比如说权重 用于如普通消息 、重要消息 排行榜应用取topN

三种特殊数据类型

Geospatial

本质是使用sorted set(zset) 只是存放的内容特殊 故可以使用对应的命令操作

地理位置信息 可以用于定位、附近的人、打车距离等应用

相关命令

# 添加地理位置  一般就经度 纬度 名称 经度纬度必须正确 有一个范围
$ GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]
summary: Add one or more geospatial items in the geospatial index represented using a sorted setgeoadd china:city 116.40 39.90 beijing
geoadd china:city 121.47 31.23 shanghai# 查看添加的指定地理位置
$ GEOPOS key member [member ...]
summary: Returns longitude and latitude of members of a geospatial indexgeopos china:city beijing shanghai# 返回两个给定位置之间的直线距离 注意带单位
$ GEODIST key member1 member2 [m|km|ft|mi]
summary: Returns the distance between two members of a geospatial indexGEODIST china:city beijing shanghai km
"1067.3788"# 返回二维经纬度对应的一维geohash字符串表示
$ GEOHASH key member [member ...]
summary: Returns members of a geospatial index as standard geohash stringsgeohash china:city beijing
1) "wx4fbxxfke0"# 查询直接给定经度纬度的基准点 给定半径范围内的 key中的成员
$ GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point# 将key中指定成员作为基准点 给定半径范围内的成员
$ GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key]
summary: Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member# 我个人觉得就是上面两个功能的集成版本 直接给经度纬度或者指定成员 指定以半径范围 还是以矩形范围
$ GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
summary: Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle.# 就是将上面的查出来的信息一并存到另一个key中
$ GEOSEARCHSTORE destination source [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH] [STOREDIST]
summary: Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle, and store the result in another key.

Hyperloglog

基数 - 符合集合定义的集合元素个数(即去重后的个数)

统计基数 - 统计一个集合的基数

问题如

统计一个页面 每天用户进入的次数(一个用户算一次)

在大数据量的情况下使用Hyperloglog

统计基数方法历程

  • Redis Set 保存数据 然后scard统计

    缺点

    • 占用内存随数据量线性增长
    • 添加元素判断是否重复的成本较高(挨个比对)
  • B树

    用B树存储要统计的数据 可以对数时间复杂度判重和插入 统计基数只需计算节点个数

    但问题在于并没有节省存储内存 例如要同时统计几万个链接的UV 每个链接的访问量都很大 如果把这些数据都维护到内存中 不太现实

  • bitmap (重要)

    用bit数组存储元素 bitmap中1的个数就是基数 可以通过位运算轻松合并多个集合(异或)

    相比set 内存占用大大减小 比如1亿数据需要内存 10000 0000 / 8 / 1024 / 1024 ≈ 12M

    表现已经不错 但如果需要统计1000个这种对象 依旧需要约12G的内存 在大数据量情况下不可行

  • hyperloglog

    实际上目前还没有发现更好的在大数据场景中准确计算基数的高效算法 因此在不追求绝对准确的情况下 使用概率算法算是一个不错的解决方案

    概率算法不直接存储数据集合本身 而是通过一定的概率统计方法估计基数值 可以大大节省内存 同时保证误差控制在一定范围内 目前用于基数统计的概率算法包括

    • Linear Counting(LC):早期的基数估计算法 在空间复杂度方面并不算优秀
    • LogLog Counting(LLC):相比于LC空间复杂度更低
    • HyperLogLog Counting(HLL):HyperLogLog Counting是基于LLC的优化和改进 在同样空间复杂度情况下 能够比LLC的基数估计误差更小

Hyperloglog原理

作用

HLL 算法需要完整遍历所有元素一次 该算法只能计算集合中有多少个不重复的元素 不能给出每个元素的出现次数或是判断一个元素是否之前出现过 多个使用 HLL 统计出的基数值可以融合

先了解伯努利试验

  • 抛硬币 连续抛多次直到抛到正面 称为一次伯努利试验
  • n次伯努利试验 抛掷次数为 k1 到 kn 其中最大值设为k_max

结合最大似然估算方法 => $n \approx 2^{k_max} $ (n越大的情况下误差越小 大数定律)

通过hash函数将数据转化为0、1组成的比特串 可以和抛硬币的正反类比

**即将一个数据的hash 类比成一次伯努利试验 所有数据hash 类比成一轮伯努利试验 **

然后再基于上面的估算结论 可以通过多次抛硬币实验的最大抛到正面的次数来预估总共进行了多少次实验

同样也就可以根据存入数据中 转化后的出现 1 的最晚位置 k_max 来估算存入了多少数据

基本思想原理如上 那么误差怎么降低到可接受范围(这种简单的推断计算出来集合的基数是有较大的偏差的)?

n次伯努利试验是一轮估算 当n足够大 误差会相对较小 但仍然不够小(即n的增加已经不能作为主要减少误差的手段 即使将其均分 均后分的n也依旧足够大 但此时将这些均分的用作多轮试验 平均结果 误差会更小 此处平均结果也不使用也不是直接平均 而是调和平均(倒数平均数的倒数)) -> 进行多轮伯努利试验

分轮 抽象到计算机中 -> 分桶

即 将长度为 L 的数组 S (单位bit)平均分为 m 组 即对应m轮 每组所占有的比特个数是平均的 设为 p

=> 总结即分桶、调和平均

那么如何将数据分发到不同的桶?

  • 分桶时设定每个比特串的前多少位转为十进制 其值对应所在桶的下标
  • 剩余位数用于求k_max

最终所有数据被分发到不同的桶 且每个桶都有一个k_max 根据估算结论 求出每个桶的基数(根据最终桶的十进制值) 再调和平均 公式如下

调和平均

基数

Redis Hyperloglog

Redis 2.8.9 引入Hyperloglog 数据结构 Redis Hyperloglog基数统计算法

特点

占用内存固定且较小 (标准误差0.81%的前提下 能够统计2^64个元素 只需12kb)这个误差在一些场景下如统计网站访问量(按用户算)是可以忽略的(传统是保存用户id 再去求数量 但单独为了求基数 则显得比较占内存)

RedisHyperLogLog设置

m=16384 p=6 L=16384 * 6 => 占用内存为=16384 * 6 / 8 / 1024 = 12K

Redis的HyperLogLog value会被hash成64位比特串 前14位用于分桶(十进制值作为桶标号)

剩下50位 获取第一个1的位置index(十进制) 将其转为二进制放进桶(如果index > current_bucket_k_max )求每个桶k_max

那么理一下 实际上只用了16384个6字节(实际上6字节 不止可以放到50 而是63)的桶 = 12k 就可以统计 2^64个数

应该是先确定转化为64位 又确定了使用16384个桶(大数据量这个分桶数量也比较合适 如果太多桶 每个桶的数据较少 反而效果可能一般了) 那么就用64 - log2(16384) = 50位来求kmax 要装下只能用6位的桶 实际上可以转化为63 + 14 = 77位的比特串

Redis使用

# 添加元素
$ pfadd key element1 element2······# 统计基数
$ pfcount key1 key2····  # 可以同时统计多个HHL结构# 将多个HLL结构中元素移动到新的HLL结构中
$ pfmerge key key1 key2····  # 将key1、key2····移动到key中

本身不存储数据 加数据进去就相当于进对应的桶 然后将k_max更新(如果满足条件) 然后会自动计算其基数

**相当于只维护一个计算基数的功能 ** 融合就相当于相同的桶取k_max更大的

Bitmap

用于统计用户信息 活跃/不活跃 登录/未登录 打卡等两个状态的比较合适 (以前通过mysql表 对象 还要通过算法统计某些需要的数据如打卡天数) 现在使用7个bit位就能监测比如一个星期的打卡情况 且能高效地查询和统计

原理在前 Redis中使用如下

$ setbit key offset value $ getbit key offset$ bitcount key start end # 统计操作 统计指定范围内为1的基数

Redis底层数据结构(重要)

Redis的多个数据类型底层如何实现?Redis本身用C编写

Redis对象模型

redis中并没有直接使用以下各种数据结构来实现键值数据库 而是基于一种对象 对象底层再间接的引用下文所说的具体的数据结构

SDS

Simple dynamic string 简单动态字符串

redis中所有场景中的字符串 基本都由SDS实现

双向链表 linkedlist

压缩列表 ziplist

redis的列表键和哈希键的底层实现之一 是为了节约内存而开发的 由连续内存块组成 减少了内存碎片和指针的内存占用

其中entry结构如下

字典

Redis 的字典使用哈希表作为底层实现 由 dict.h/dictht 结构定义 使用开链法解决哈希冲突

整体结构

蓝色为哈希表 有两个: ht[0] 和 ht[1] 主要目的是为了完成rehash

rehash

在扩容时 将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面 完成之后释放空间并交换两个 dictht 的角色

渐进式rehash

rehash 操作不是一次性完成 而是采用渐进方式 这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担

渐进式 rehash 通过记录 dict 的 rehashidx 完成 它从 0 开始 然后每执行一次 rehash 都会递增

例如在一次 rehash 中 要把 dict[0] rehash 到 dict[1] 这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上 dict[0] 的 table[rehashidx] 指向 null 并令 rehashidx++

在 rehash 期间 每次对字典执行添加、删除、查找或者更新操作时 都会执行一次渐进式 rehash(分散rehash操作)

采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上 因此对字典的查找操作也需要到对应的 dictht 去执行

intset

集合键的底层实现方式之一

跳表(重要)

有序集合的底层实现之一

跳跃表是基于多指针有序链表实现的 可以看成多个有序链表

  • 由很多层结构组成

  • 每一层都是一个有序的链表 排列顺序为由高层到底层 都至少包含两个链表节点 head节点和后面的nil节点

  • 最底层的链表包含了所有的元素

  • 如果一个元素出现在某一层的链表中 那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集)

  • 链表中的每个节点都包含两个指针 一个指向同一层的下一个链表节点 另一个指向下一层的同一个链表节点

查找

从最高层的链表节点开始 如果比当前节点要大和比当前层的下一个节点要小 那么则往下找 也就是和当前层的下一层的节点的下一个节点进行比较 以此类推 一直找到最底层的最后一个节点 如果找到则返回 反之则返回空

插入

首先确定插入的层数 有一种方法是假设抛一枚硬币 如果是正面就累加 直到遇见反面为止 最后记录正面的次数作为插入的层数 当确定插入的层数k后 则需要将新元素插入到从底层到k层

删除

在各个层中找到包含指定值的节点 然后将节点从链表中删除即可 如果删除以后只剩下头尾两个节点 则删除这一层

Redis跳跃表实现

Redis 的跳跃表由 redis.h/zskiplistNode 和 redis.h/zskiplist 两个结构定义 其中 zskiplistNode 结构用于表示跳跃表节点 而 zskiplist 结构则用于保存跳跃表节点的相关信息 比如节点的数量以及指向表头节点和表尾节点的指针

Redis事务

Redis事务实现

事务本质上就满足ACID

Redis单条命令是原子性的 但是事务不保证原子性 而且其事务也没有隔离级别的概念

Redis事务:一个事务中的所有命令被序列化 一次性按照顺序执行 (一次性、顺序性、排他性)

实现:

  • 开启事务(multi)
  • 命令入队(…)
  • 执行事务(exec)
# 正常流程
MULTI
set k1 v1
set k2 v2
get k1
exec
# discard 放弃执行事务# 事务中一条命令代码错误 -> 整个事务都不执行
MULTI
set k3 1
get k3
set k4 "test"
incy k4
-> ERR unknown command `incy`, with args beginning with: `k4`,
exec
# EXECABORT Transaction discarded because of previous errors.# 事务中有命令运行时异常 -> 其他命令正常执行
MULTI
set k3 "t"
set k4 1
get k4
get k3
incr k3
get k3
exec
1) OK
2) OK
3) "1"
4) "t"
5) (error) ERR value is not an integer or out of range
6) "t"

监控

悲观锁、乐观锁

Redis使用watch 实现乐观锁的作用 watch key …

则在当前事务没有实际执行(包含对该key的操作)之前 若有另外的线程改变了监控的值 则当前事务执行失败(相当于取Version没有符合条件)那么需要重新watch(先unwatch)再去执行 相当于自旋乐观锁

Jedis

原生操作

Java操作Redis的一个中间件 本质上就是利用Java去调用客户端的指令(类似JDBC?)

Maven中使用导入Jedis依赖即可

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.0.0</version>
</dependency>

远程连接服务器的Redis 需要

  • 服务器开启端口
  • 修改其配置文件以接受其他主机连接

个人已经连接成功 为了安全将其关闭 使用本地测试

Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.set("username", "testRemote");
System.out.println(jedis.ping());jedis.close();

其方法即是命令行相关指令的实现 对应即可

Jedis实现事务示例

Transaction transaction = jedis.multi();
try {transaction.set("k1", "v1");transaction.set("k2", "v2");transaction.set("k3", "1");transaction.incr("k3");transaction.incr("k2");  // 测试运行时异常
} catch (Exception e) {transaction.discard();e.printStackTrace();
} finally {transaction.exec();System.out.println(jedis.get("k1"));System.out.println(jedis.get("k2"));System.out.println(jedis.get("k3"));System.out.println(jedis.dbSize());jedis.flushDB();
}

SpringBoot整合Redis

可以手动导入依赖 (现在知道原理了就直接通过SpringBoot选项创建即可)

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

SpringBoot 2.x 以后 其底层使用的Jedis被替换为lettuce

jedis : 采用直连方式 多线程下操作不安全 若想避免不安全 使用 jedis pool 连接池

lettuce:采用netty的 实例可以被多个线程共享 不存在线程不安全的情况 可以减少线程

整合原理

Redis对应的自动配置类和Properties类

并且提供了RedisTemplate 直接供我们使用

则实现

配置(简单形式

spring:redis:host: 127.0.0.1port: 6379# lettuce# 集群等

使用

Redis.conf 详解

  1. 配置文件unit的存储显示单位 大小写不敏感

  2. 可以包含其他配置文件

  3. 网络

    bind 127.0.0.1 # 绑定ip 可以配置多个 *
    protected-mode yes # 保护模式 默认yes 不允许其他主机连接
    port 6379 # 访问端口
    
  4. 通用

    daemonize yes # 后台运行# 后台方式运行 需要指定一个pid文件
    pidfile /var/run/redis_6379.pid# 日志
    # debug (a lot of information, useful for development/testing)
    # verbose (many rarely useful info, but not a mess like the debug level)
    # notice (moderately verbose, what you want in production probably)
    # warning (only very important / critical messages are logged)
    loglevel notice # 日志级别
    logfile "" # 日志文件地址databases 16 # 默认16个数据库
    
  5. 快照 SNAPSHOTTING RDB

    # 3600s内 若有一个key进行了修改 就进行rdb持久化操作
    save 3600 1
    # save 300 100
    # save 60 10000stop-writes-on-bgsave-error yes # 持久化出错了 是否继续工作
    rdbcompression yes # 是否压缩rdb文件 会消耗一些CPU
    rdbchecksum yes # 保存rdb文件时 是否进行错误校验
    dbfilename dump.rdb # rdb文件名
    rdb-del-sync-files no # rdb在没有持久性的情况下删除复制中使用的RDB文件 通常情况下保持默认即可
    dir ./ # 目录
    
  6. 复制 REPLICATION (主从复制)

    replicaof <masterip> <masterport> # 指定当前节点(从节点)的主节点ip 主节点端口号
    
  7. 客户端 Clients

    requirepass foobared # 默认不开启 设置密码后 需用auth验证
    # maxclients 10000  # 最大连接数
    
  8. 内存管理 MEMORY MANAGEMENT

    # maxmemory <bytes>
    maxmemory-policy noeviction # 内存满了时候执行的策略 重要
    # volatile-lru -> 从已设置过期时间的数据集中挑选最近最久未使用的数据淘汰
    # allkeys-lru -> 从数据集中挑选最近最久未使用使用的数据淘汰
    # volatile-lfu -> 从已设置过期时间的数据集挑选使用频率最低的数据淘汰
    # allkeys-lfu -> 从数据集中挑选使用频率最低的数据淘汰
    # volatile-random -> 从已设置过期时间的数据集中任意选择数据淘汰
    # allkeys-random -> 任意选择一个key淘汰
    # volatile-ttl -> 从已设置过期时间的数据集中挑选将要过期的数据淘汰
    # noeviction -> 禁止淘汰数据 默认策略
    
  9. AOF APPEND ONLY MODE

    appendonly no # 默认不开启
    appendfilename "appendonly.aof" # aof文件名# appendfsync always
    appendfsync everysec # 持久化策略 每秒执行一次
    # appendfsync nono-appendfsync-on-rewrite no # rewrite操作时 appendfsync会被阻塞 默认即可 保证数据安全性auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    

    具体到AOF持久化中

RDB持久化(重要)

Redis Database

Redis默认开启

Motivation

Redis是内存数据库 不持久化则数据断电即失 故Redis提供持久化功能

基本思想

在指定时间间隔内 将内存中的数据的快照写入磁盘 恢复时将快照文件恢复到内存即可

过程(bgsave)

Redis单独fork一个子进程进行持久化 其会将数据写入一个临时文件 写入完成后再用这个临时文件替换以前的持久化文件

触发机制

  • 配置文件中save的规则满足情况下 触发save 如 save 60 5 - 60s内5次修改就会自动持久化
  • 执行flushall 触发
  • 退出redis server 触发

触发后会生成一个dump.rdb

优点

  • 主进程不进行任何IO操作 性能极高
  • 适合大规模数据恢复 且对于恢复完整性不是很敏感

缺点

  • 最后一次持久化的数据可能丢失 (意外宕机)
  • fork进程 占用一定资源

原理

fork + copyonwrite

子进程共享主进程的物理空间 当主进程有内存写入操作时 read-only内存页发生中断 将触发的异常的内存页复制一份(其余的页还是共享主进程的)

不会竞争磁盘IO 因为父进程不会写磁盘

AOF持久化(重要)

Append Only File

将执行过的所有写命令都记录到一个文件 恢复的时候将这个文件全部执行一遍 (每次添加 追加形式)

使用相关参数

appendonly no # 默认不开启 设置为yes开启
appendfilename "appendonly.aof" # aof文件名

由于OS会在内核中缓存write做的修改 不是立即写到磁盘上(每隔30秒将文件写入到磁盘中) 则实际上aof方式的持久化也还是有可能会丢失部分修改 不过我们可以通过配置文件告诉redis我们想要通过fsync函数强制os写入到磁盘的时机

持久化策略

# appendfsync always
appendfsync everysec # 持久化策略 每秒执行一次
# appendfsync no
  • always 每一个操作都同步 完整性更好 性能不高
  • everysecc 每秒执行一次 可能丢失一秒数据 折中
  • no 不同步 这个时候OS自己同步数据 速度最快 效率最高 持久化无保证

AOF修复

aof文件可能被意外或无意中修改 这时redis无法启动 需要将其变为正常 可以使用redis-check-aof --fix 修复 可能会丢失一部分数据

重写

AOF的原理是直接把用户插入到服务器的命令追加到结尾 那么文件会原来越大 一些重复的写命令也会越来越多 可以利用BGREWRITEAOF 命令来重写AOF 相关配置如下 (现在的版本会自动去重写)

no-appendfsync-on-rewrite no # rewrite操作时 appendfsync会被阻塞 默认即可 保证数据安全性auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

当aof文件超过设置大小乘以(1+比例) fork一个子进程进行重写(将其内存中的数据以命令的形式保存到一个临时的aof文件中)

存在一个问题

bgrewriteaof往往会涉及大量磁盘操作 持续时间一般比较长 子进程往新aof文件中写入数据的过程中 如果父进程有新的写操作也需要写入到原aof中 两者都会操作磁盘 会竞争IO

解决

Redis为AOF重写提供一个重写缓冲区以及配置一个 no-appendfsync-on-rewrite参数来控制父进程如何处理写操作

重写时 Redis会把写操作同时追加到aof缓冲区(内核)和aof重写缓冲区

  • AOF缓冲区中的内容仍会根据fsync同步策略被同步到原aof磁盘文件中 对原有aof文件的处理照常进行

  • 对于重写缓冲区中的数据 在重写完成后子进程会向父进程发送一个信号 父进程接收到该信号后再将aof重写缓冲区中的内容写入新aof文件中 最后重命名新的aof文件覆盖原有aof

  • 如果该参数设置为no 是最安全的方式 不会丢失数据 但是要忍受阻塞的问题

  • 如果设置为yes 就相当于将appendfsync设置为no 这说明并没有执行磁盘操作 只是写入了缓冲区 因此这样并不会造成阻塞(因为没有竞争磁盘)但是如果这个时候redis挂掉 就会丢失数据 丢失多少数据呢 在linux的操作系统的默认设置下 最多会丢失30s的数据

原理

AOF缺点

  • 相对rdb来说文件大很多 修复速度也慢很多
  • 运行效率也比rdb慢

RDB、AOF 组合

共存的情况下 出于数据完整性的考虑 Redis会以aof文件为主 通过加载aof文件来重构数据 如果aof文件出错则启动失败

对于RDB 官方认为可以充当一个备份数据库的角色 应对AOF可能存在的潜在Bug 启动失败时可以通过RDB快速重启

性能建议

  • RDB只用作备份 建议只在Slave上持久化RDB文件 建议15分钟备份一次 save 900 1
  • 若开启AOF 好处是最差情况下也不会丢失超过两秒的数据 代价一是持续的IO 二是rewrite造成的阻塞几乎不可避免 应尽量在硬盘允许的情况下减少重写频率 即将其重写阈值调整一下 一般5G
  • 若不开启AOF 仅靠主从复制实现高可用也可以 省去了大部分IO 也没有rewrite导致系统波动 代价是如果主从同时挂掉 会丢失十几分钟的数据 启动脚本也要比较主从的RDB文件 载入较新的那个 微博是这种架构

Redis发布订阅

发布订阅是一种消息通信模式 核心是队列

发布者(pub)发布消息 订阅者(sub)接收消息 如微信、微博、关注系统等

Redis客户端可以订阅任意数量的频道

拓扑结构

角色

  • 消息发布者
  • 频道
  • 消息订阅者

相关命令

$ help @pubsub# 发布一个消息到一个频道
$ PUBLISH channel message
$ summary: Post a message to a channel
$ since: 2.0.0# 订阅指定频道的消息
$ SUBSCRIBE channel [channel ...]
$ summary: Listen for messages published to the given channels
$ since: 2.0.0# 取消订阅
$ UNSUBSCRIBE [channel [channel ...]]
$ summary: Stop listening for messages posted to the given channels
$ since: 2.0.0# 监听发布到指定频道(符合指定模式)的消息
$ PSUBSCRIBE pattern [pattern ...]
$ summary: Listen for messages published to channels matching the given patterns
$ since: 2.0.0# 取消监听
$ PUNSUBSCRIBE [pattern [pattern ...]]
$ summary: Stop listening for messages posted to channels matching the given patterns
$ since: 2.0.0# 查看订阅发布系统状态
$ PUBSUB subcommand [argument [argument ...]]
$ summary: Inspect the state of the Pub/Sub subsystem
$ since: 2.8.0

原理

Redis使用C实现发布订阅 - pubsub.c

Redis 通过publish、subscribe、psubscribe 等命令实现发布和订阅功能

**redis-server维护一个字典 key是channel 值是一个链表 链表中保存所有订阅这个channel的客户端 **

  • 订阅者通过subscribe命令订阅某频道后 即将订阅者添加到指定channel的订阅链表中
  • 发布者通过publish将消息发布到指定频道 redis-server会遍历对应channel的订阅链表 将消息发布给所有订阅者

使用场景

  • 实时消息系统
  • 实时聊天(频道当作聊天室 将信息回显给所有人即可)
  • 订阅、关注系统

较复杂的场景会使用消息中间件来做 如RabbitMQ

Redis主从复制(重要)

Motivation:

主从复制、读写分离 实际中80%对数据库的访问是读请求 在访问量较大时 为了减轻服务器压力 使用几个从服务器接收读请求 主服务器接收写请求 主服务器单向复制到从服务器

默认情况下 每台Redis服务器都是主节点 (需要去配置主从关系)主节点和从节点一对多

作用:

  • 数据冗余: 实现了 数据的热备份 持久化外的一种冗余方式
  • 故障恢复 :主节点出现故障 可以由从节点提供服务 实际是服务的冗余
  • 负载均衡 :配合读写分离 减轻了服务器压力 提高了并发量
  • 高可用基石: 主从复制是哨兵和集群实施的基础

复制原理

Slave启动后连接到Master时会发送一个sync同步命令

Master收到命令 启动后台存盘进程 同时将此时收到的写命令放入缓冲区 存盘完毕后 Master传输整个文件到Slave 发送完毕后 开始向从节点发送存在缓冲区的写命令 完成一次完全同步

  • 全量复制 - Slave在收到文件数据后 将其存盘并加载到内存
  • 增量复制 - Master继续将后来的修改命令传给Slave 继续同步

Redis查看当前节点主从复制相关信息

$ info replication
# Replication
role:master
connected_slaves:0
...

简单实现一主二从

  • 拷贝三份配置文件 修改端口号 pid文件名、 dump文件名等相关配置
  • 启动三个redis服务
  • 选取两个服务作为从节点 配置其属于的主节点 Slaveof host port

这样是临时去配置 如果要实现永久 那么在从节点的配置文件中配置

replicaof <masterip> <masterport> # 指定当前节点(从节点)的主节点ip 主节点端口号

主从链

随着负载不断上升 主服务器可能无法很快地更新所有从服务器 或者重新连接和重新同步从服务器将导致系统超载 为了解决这个问题 可以创建一个中间层来分担主服务器的复制工作

主从节点特点

主节点写 从节点读

若主节点断开再连接回来 从节点依旧可以获取到主机写的数据 但主节点断开时间未知 最好选举新的主节点 如果此时想变为主节点 那么可以手动使用Slave no one 命令 其他从节点也手动重新从属于该节点 如果想实现自动化选举主节点 -> 哨兵模式

哨兵模式

主节点宕机 手动切换从节点为主节点 人工干预 费时费力 还会造成一段时间服务不可用

Redis2.8 引入哨兵模式 - Sentinel

Sentinel是一个独立的进程 能够监控节点是否故障 如果故障了还能根据投票数自动将某个从节点转为主节点

原理

  • 哨兵通过发送命令 等待Redis服务器响应 从而监控运行的多个实例
  • 当哨兵监控到主节点宕机 根据投票将Slave切换成Master 然后通过发布订阅模式通知其他从节点修改配置文件 切换主机

一个哨兵进程本身也可能挂掉 为此 使用多个哨兵进行监控 各个哨兵之间也会监控

假设主节点宕机 哨兵1先检测到 系统不会立即failover(故障转移)这时称为主观下线 而是要一定数量的哨兵都检测到主节点不可用 哨兵之间就会进行一次投票 有结果后由一个哨兵进行failover 切换成功后 通过发布订阅模式 让各个哨兵把自己监控的从节点实现切换主机 称为客观下线

实现

  • 编写哨兵配置文件 sentinel.conf (主要配置本身端口 监控的节点等信息
  • 开启哨兵 redis-sentinel rconfig/sentinel.conf

示例配置文件

# 哨兵sentinel实例运行的端口 默认26379
port 26379# 哨兵sentinel的工作目录dir /tmp# 哨兵sentinel监控的redis主节点的 ip port
# master-name  可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越多的slave因为replication而不可用
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等)将会去调用这个脚本 这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数 一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

优点

  • 高可用

缺点

  • 当集群容量达到上限 在线扩容十分麻烦
  • 实现哨兵模式配置麻烦

Redis缓存(重要)

Redis缓存的使用 极大提升了应用的性能 尤其是查询 但带来的问题是缓存一致性问题、缓存穿透、缓存雪崩、缓存击穿、缓存无底洞等

缓存一致性

缓存一致性要求数据更新的同时缓存数据也能够实时更新

解决方案

  • 在数据更新的同时立即去更新缓存
  • 在读缓存之前先判断缓存是否是最新的 如果不是最新的先进行更新

要保证缓存一致性需要付出很大的代价 缓存数据最好是那些对一致性要求不高的数据 允许缓存数据存在一些脏数据

缓存穿透

请求了一个一定不存在的数据 该请求会穿透缓存到达数据库

解决:

  • 对这类数据缓存一个空数据
  • 过滤这类请求

如布隆过滤器 利用高效的数据结构和算法快速判断请求的key是否存在于数据库 绝对不存在则直接Return 可能存在才去查询

缓存雪崩

指数据还没有加载到缓存中、或缓存同一时间大面积失效、或缓存服务器宕机 导致大量请求同时到达数据库 导致数据库崩溃

解决:缓存预热、合理设置缓存过期时间(加上一个随机值)、分布式缓存(高可用 集群异地多活、限流降级(加锁或者队列来控制读数据库写缓存的线程数)

缓存击穿

类似雪崩 不同的是击穿指一个key非常热点 大并发集中对这一个点进行访问 该key失效瞬间持续的大并发就击穿缓存 直接请求数据库

解决: 热点数据永不过期、加互斥锁(保证一个线程去数据库查询)

缓存无底洞

指的是为了满足业务要求添加了大量缓存节点 但是性能不但没有好转反而下降了的现象

原因:缓存系统通常采用 hash 函数将 key 映射到对应的缓存节点 随着缓存节点数目的增加 键值分布到更多的节点上 导致客户端一次批量操作会涉及多次网络操作 这意味着批量操作的耗时会随着节点数目的增加而不断增大 此外 网络连接数变多 对节点的性能也有一定影响

解决:

  • 优化批量数据操作命令
  • 减少网络通信次数
  • 降低接入成本 使用长连接 / 连接池,NIO 等

一致性哈希

Distributed Hash Table(DHT) 是一种哈希分布方式 其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题 :

缓存需求增加时 需要增加缓存节点 而传统哈希分布其节点 -> hash(key) % N 与节点数有关 则迁移时大量缓存的位置需要改变 在一定时间内大量缓存失效 造成缓存雪崩 这是Hash算法本身的原因 -> 一致性哈希

一致性哈希算法是对2^32取模

服务器使用其各自IP进行哈希 结果对2^32取模 然后对应圆环上的一个位置

那么一个对象应该被缓存到哪台服务器上? 将缓存服务器与被缓存对象都映射到hash环上以后 从被缓存对象的位置出发 沿顺时针方向遇到的第一个服务器 就是当前对象将要缓存于的服务器

这样就算服务器数量改变 也只有少量缓存失效

但一个问题在于服务器可能映射到环上的位置并不均匀 很可能造成大量缓存集中到一个服务器上 缓存极度不均匀 仍然可能造成系统的崩溃 称为hash环的偏斜

使用虚拟节点解决

虚拟节点”是”实际节点”(实际的物理服务器)在hash环上的复制品 一个实际节点可以对应多个虚拟节点

Redis线程模型

文件事件处理器(file event handler)

Redis 基于 Reactor 模式开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler)

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
  • 文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。

个人理解就是文件事件处理器 是selector

更细化

Redis是单线程模型为什么效率还这么高?

  • 纯内存访问:数据存放在内存中,内存的响应时间大约是100纳秒,这是Redis每秒万亿级别访问的重要基础。
  • 非阻塞I/O:Redis采用epoll做为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了时间,不在I/O上浪费过多的时间。
  • 单线程避免了线程切换和竞态产生的消耗。
  • Redis采用单线程模型,每条命令执行如果占用大量时间,会造成其他线程阻塞,对于Redis这种高性能服务是致命的,所以Redis是面向高速执行的数据库

Redis Lua

场景

Redis的单个命令都是原子性的,有时候我们希望能够组合多个Redis命令,并让这个组合也能够原子性的执行,甚至可以重复使用,在软件热更新中也有一席之地。Redis在2.6版本中引入了一个特性来解决这个问题,这就是Redis执行Lua脚本

Lua

Lua的简单语法

建议使用的类型

  • nil

  • boolean

  • string

  • number

  • table(结合map和数组

    > arr_table = {'felord.cn','Felordcn',1,age = 18,nil}
    > print(arr_table[1])
    felord.cn
    > print(arr_table[4])
    nil
    > print(arr_table['age'])
    18
    > print(#arr_table)
    3
    

    但尽量避免使用混合模式的table并尽量避免元素为nil 此外#取table长度不一定准确 不确定元素的情况下应使用循环计算长度判断、循环

local a = 10
if a < 10  thenprint('a小于10')
elseif a < 20 thenprint('a小于20,大于等于10')
elseprint('a大于等于20')
endlocal arr = {1,2,name='felord.cn'}for i, v in ipairs(arr) doprint('i = '..i)print('v = '.. v)
endprint('-------------------')for i, v in pairs(arr) doprint('p i = '..i)print('p v = '.. v)
end

Redis Lua

Redis使用EVAL命令执行指定的Lua脚本

EVAL luascript numkeys key [key ...] arg [arg ...]
# `numkeys`无论什么情况下都是必须的命令参数127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> EVAL "return redis.call('GET',KEYS[1])" 1 hello
"world"# 通过redis.call()来执行了一个SET命令,其实我们也可以替换为redis.pcall()。它们唯一的区别就在于处理错误的方式,前者执行命令错误时会向调用者直接返回一个错误;而后者则会将错误包装为一个table# 由于在Redis中存在Redis和Lua两种不同的运行环境,在Redis和Lua互相传递数据时必然发生对应的转换操作,这种转换操作是我们在实践中不能忽略的。例如如果Lua脚本向Redis返回小数,那么会损失小数精度;如果转换为字符串则是安全的。
127.0.0.1:6379> EVAL "return 3.14" 0
(integer) 3
127.0.0.1:6379> EVAL "return tostring(3.14)" 0
"3.14"

Lua脚本管理

Lua脚本在Redis中是以原子方式执行的 因此不宜编写一些复杂逻辑 必须保证其效率 以免影响其他客户端

  • SCRIPT LOAD

    # 加载脚本到缓存以达到重复使用,避免多次加载浪费带宽,每一个脚本都会通过SHA校验返回唯一字符串标识。需要配合`EVALSHA`命令来执行缓存后的脚本
    127.0.0.1:6379> SCRIPT LOAD "return 'hello'"
    "1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"
    127.0.0.1:6379> EVALSHA 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 0
    "hello"
    
  • SCRIPT FLUSH

  • SCRIPT EXISTS

  • SCRIPT KILL

注意点

  • 务必对Lua脚本进行全面测试以保证其逻辑的健壮性,当Lua脚本遇到异常时,已经执行过的逻辑是不会回滚的。
  • 尽量不使用Lua提供的具有随机性的函数,参见相关官方文档。
  • 在Lua脚本中不要编写function函数,整个脚本作为一个函数的函数体。
  • 在脚本编写中声明的变量全部使用local关键字。
  • 在集群中使用Lua脚本要确保逻辑中所有的key分到相同机器,也就是同一个插槽(slot)中,可采用Redis Hash Tag技术。
  • 再次重申Lua脚本一定不要包含过于耗时、过于复杂的逻辑。

Redis分布式锁

这次抽奖红包提现需求中遇到了需要分布式锁的场景 - 防止并发竞争导致申请提现时数据库写入状态错误 从而导致不正确提现

而Redis非常适合作为分布式锁实现使用 分布式锁应该具有的作用

  • 互斥性: 任意时刻,只有一个客户端能持有锁。
  • 锁超时释放:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
  • 可重入性:一个线程如果获取了锁之后,可以再次对其请求加锁。
  • 高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
  • 安全性:锁只能被持有的客户端删除,不能被其他客户端删除

Redis分布式锁的七种实现方式

  1. SETNX + EXPIRE
  2. SETNX (value = 系统当前时间 + 过期时间)
  3. LUA(SETNX + EXPIRE)
  4. SET扩展命令 SET NX、EX、PX、XX
  5. SET EX 等 + 校验唯一随机值
  6. Redisson框架
  7. RedLock + Redisson

特点及存在的问题

  1. 实现简单 支持锁重入 锁超时自动释放 但非原子性操作 在中间崩溃可能导致锁无法释放

  2. 加锁原子性操作 但实现复杂 机器间的系统时间必须同步 其他加锁线程可能修改过期时间 锁可能被其他线程释放

  3. 加锁和解锁原子性 但不支持锁重入 锁无法自动续期 主从模式下可能造成锁丢失

  4. 加锁和解锁原子性 同样不支持锁重入 锁无法自动续期 可能锁过期释放但业务没执行完 锁有可能被其他线程释放

  5. 相对4给value设置一个标记当前线程唯一的随机数 在删除时进行校验 防止误删 但依旧存在锁过期释放但业务未执行完毕的问题

  6. 针对锁过期释放但业务未执行完毕 Redisson框架启动一个Watch dog线程用于定时间隔检查线程是否还持有锁 如果持有则延长锁的有效时间

  7. 以上方法均为单机版本考虑 考虑多机版本 存在一个问题:单master如果挂掉 在其上申请的锁 对后续升级成master的节点没有作用 即其他线程可以在后续的master上获取同个锁 锁的安全性出现问题 为了解决则引入RedLock分布式锁算法 Redisson实现了RedLock版本的锁

RedLock分布式锁算法

  1. 获取当前时间,以毫秒为单位。
  2. 按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间, 此处假设超时时间是50ms)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
  3. 客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间需要减去获取锁所使用的时间
  5. 如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁)

简化步骤即:

  • 按顺序向N个master节点请求加锁
  • 根据自定的超时时间判断是否跳过当前master节点
  • 如果大于等于N/2 + 1个节点加锁成功 并且使用的时间小于锁的有效期 即可认定加锁成功
  • 如果获取锁失败则解锁

Redis个人简单总结相关推荐

  1. Redis的简单实践

    Redis的简单实践 文章目录 Redis的简单实践 前言 Redis简介 Redis基本操作命令 Java使用Redis 使用IDEA搭建Redis项目 使用Jedis进行简单增删改查 使用Jedi ...

  2. redis简单队列java_使用Redis的简单消息队列

    redis简单队列java 在本文中,我们将使用列表命令将Redis用作简单的消息队列. 假设我们有一个允许用户上传照片的应用程序. 然后在应用程序中,我们以不同大小显示照片,例如Thumb,Medi ...

  3. 使用Redis的简单消息队列

    在本文中,我们将使用列表命令将Redis用作简单的消息队列. 假设我们有一个允许用户上传照片的应用程序. 然后在应用程序中,我们以不同大小显示照片,例如Thumb,Medium和Large. 在第一个 ...

  4. java签到程序设计_java redis 实现简单的用户签到功能

    业务需求是用户每天只能签到一次,而且签到后用户增加积分,所以把用户每次签到时放到redis 缓存里面,然后每天凌晨时再清除缓存,大概简单思想是这样的 直接看代码吧如下 @Transactional @ ...

  5. php redis下单,redis 队列简单实现高并发抢购/秒杀

    redis 队列简单实现高并发抢购/秒杀 2019-03-21 14:34 阅读数 82 前提为每人限购1件 <>开抢前 把秒杀商品库存存进 Redis 队列中 $redis = new ...

  6. redis学习 -- 简单动态字符串

    Redis没有使用C语言字符串的形式,通过'\0'作为结尾,而是使用了简单动态字符串(simple dynamic string). 当Redis使用的字符串不需要修改字符串的内容的时候,可以使用C语 ...

  7. redis做简单mq的高可用

    redis集群环境 生产者有多个 消费者有多个 两边随时可增加 redis上消息只会被一个消费者消费,不会有多个订阅者消费同一个消息,简单一对一 解决: 消费者崩溃问题:RPOPLPUSH保证不会由于 ...

  8. python爬虫的硬件配置_python爬虫之redis环境简单部署

    Redis 简介 Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库. Redis 与其他 key - value 缓存产品有以下三个特点: Redis支持数据的持久 ...

  9. linux suse 安装redis,在openSuse linux上Redis安装简单步骤

    1. 新建虚拟机,安装openSUSE-11.1 2. 安装后,关闭防火墙,这样ssh才可以连接 SuSEfirewall2 stop 3. ssh连接后,开始安装 redis 进入目录: cd /o ...

  10. SpringBoot连接Redis超简单

    建立一个springboot项目,什么也不用设置,建立一个最简单的就可以的,首先还是加入我们的Maven驱动包 <!-- 加入redis连接池--> <dependency>& ...

最新文章

  1. python软件怎么使用-Python快速入门—如何选择使用包管理工具?
  2. 丛高教授《空间数据管理和挖掘及在智慧城市的应用》演讲笔记
  3. python文件编译_将c程序编译为python扩展,生成.whl文件
  4. python + pyqt5 UI和信号槽分离方法
  5. Android开发笔记(八十五)手机数据库Realm
  6. https无法访问 宝塔_解决宝塔面板开启自带免费Let's Encrypt SSL证书后网站无法访问...
  7. 清除dnf垃圾进程 .bat文件
  8. 锁卡,每插入一张新卡都需要进行解锁
  9. 微信小程序数据库一次查询多个条件的方法
  10. 金蝶EAS,序时簿ListUI只允许选择一行或至少选择一行记录
  11. c语言互不相同删除法,GitHub - MXHDOIT/C_Practice: 100道C语言经典习题
  12. colorAccent,colorPrimary,colorPrimaryDark……来这里你就明白了
  13. 设定Applocker和解决问题
  14. Windows XP 下载
  15. 超市服务器操作系统,超市收银系统 服务器 配置
  16. RFID第一期——各种IC卡ID卡详解
  17. 什么是HSV色彩空间
  18. MySQL数据库——索引机制及其优化
  19. Windowsnbsp;Servernbsp;2003nbsp;SP2企…
  20. 基于VHDL的层次化设计:异步清零和同步使能4位十六进制加法计数器和七段显示译码器的元件例化实现

热门文章

  1. iTOP-3A5000开发板,龙芯处理器,规格参数
  2. 使用ESP12和Arduino开发板制作一款基于物联网IoT的电能表
  3. java实现设置Excel下拉框在使用Excel的时候用到了下拉框,实现的效果如下↓
  4. CADD计算机辅助药物设计技术——Pymol、 ChemDraw、
  5. 重节点对B样条曲线的影响
  6. 求生之路怎么修改服务器人数,求生之路2服务器怎么设置人数
  7. C++ 单继承 父类和子类
  8. 【交通标志识别】HOG特征机器学习交通标识识别【含Matlab源码 2200期】
  9. pytorch的环境配置及安装(包括anaconda的安装)
  10. 实验三 可综合时序逻辑电路实验