ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

位图、HyperLogLog、GEO 与持久化、主从、哨兵、集群高可用实战

位图、HyperLogLog、GEO 与持久化、主从、哨兵、集群高可用实战 一、Bitmap 位图1.1 本质与用途Bitmap 的本质是字符串它的特殊之处在于可以按比特位bit操作。Redis 允许我们直接读写字符串的每一个二进制位非常适合做海量数据的独立用户统计。例如字符串big的三个字符在内存中对应如下二进制b i g 01100010 01101001 01100111setkey big# 存入字符串 biggetbit key0# 取第 0 个比特位返回 0getbit key1# 取第 1 个比特位返回 1setbit key offset value# 给位图指定索引设置值0/1setbit key71# 把第 7 位设为 1big 就变成了 cigget key# 返回 cig注意位图本质上还是一个字符串命令需要在能存储二进制的键上使用setbit会自动扩展字符串长度。1.2 独立用户统计日活当用户量足够大时用位图统计日活能极大节省内存。把用户 id 作为位图的下标用户登录时setbit login:20260829 user_id 1。晚上统计bitcount login:20260829得到当天登录人数。内存换算1字节 8 个比特位可表示2^8 256种变化8字节 64 个比特位可表示2^64种变化。对比使用集合存储 1 亿用户约需 200MB使用位图仅需约12.5MB。# 获取字符串中 1 的个数可以在指定字节范围内统计bitcount key024位图适用范围适合用户 id 是连续整数且值相对集中的场景如果用户 id 十分稀疏例如 1 亿用户里只有 1000 人位图可能浪费空间此时更适合用下面介绍的 HyperLogLog。二、HyperLogLog 基数统计2.1 概念HyperLogLog 基于概率算法能用极小的空间完成独立数量基数统计本质仍是一种字符串结构。它不存储具体元素只记录有多少个不同元素。# 放值pfadd uuidsuuid1uuid2uuid3uuid4# 统计互不相同的个数pfcount uuids# 判断某个值是否已存在已存在返回 0不存在返回 1pfadd uuidsuuid12.2 常见应用日活统计每天把登录用户放入一个 HyperLogLogpfcount即当天 UV。URL 去重爬虫把网址放入集合返回 1 说明没爬过继续爬返回 0 说明爬过跳过。黑白名单 / 垃圾邮件过滤快速判断一个号码或邮箱是否在名单内。2.3 注意事项百万级独立用户统计百万条数据只占约15KB。统计存在约0.81% 的误差不追求绝对精确。无法取出单条数据只能统计个数。结构上类似布隆过滤器但算法不同HyperLogLog 侧重计数布隆过滤器侧重判断是否存在。2.4 补充布隆过滤器布隆过滤器是一种通过多个哈希函数映射到一张位数组的数据结构能快速判断一个元素是否在集合内时间和空间效率都很高典型场景爬虫 URL 去重。原理步骤开辟一个m位的 bitArray初始全部为 0元素进入时用多个哈希函数h1、h2、h3…算出多个哈希值将对应下标位置的 0 置为 1查询时同样计算多个哈希值只要发现任意一位为 0则元素一定不存在全为 1 则可能存在有小概率误判。Redis 本身不内置布隆过滤器需要借助第三方插件如 RedisBloom实现。2.5 动态链接库补充C / C 开发的程序会编译成可执行文件和动态链接库文件。常见形式Python 使用打包后的包Java 使用xx.jar。动态链接库在不同系统的后缀Windows.dllmacOS、Linux、Android.so三、GEO 地理位置3.1 概念GEO 用于存储经纬度支持计算两地距离、查询某点方圆范围内的成员典型场景是外卖、附近的人、地图签到等。北京116.28 , 39.55 天津117.12 , 39.08可以计算北京到天津的距离也能查出天津周围 50km 有哪些城市。3.2 经纬度从哪来Web 端先用 JS 获取浏览器定位再通过接口传给后端后端存入 Redis。Android / iOS 端使用 SDK 提供的定位能力。微信小程序使用小程序提供的获取经纬度接口。if(navigator.geolocation){navigator.geolocation.getCurrentPosition(function(position){varlatitudeposition.coords.latitude;varlongitudeposition.coords.longitude;console.log(经度longitude);console.log(纬度latitude);});}else{console.log(浏览器不支持Geolocation API);}3.3 常用命令# 增加地理位置信息geoadd key 经度 纬度 membergeoadd cities:locations116.2839.55beijing geoadd cities:locations117.1239.08tianjin geoadd cities:locations114.2938.02shijiazhuang geoadd cities:locations118.0139.38tangshan geoadd cities:locations115.2938.51baoding# 获取某成员经纬度geopos key membergeopos cities:locations tianjin# 计算两个成员之间的距离geodist key m1 m2 单位geodist cities:locations beijing tianjin km# 约 89 公里# 查询某成员方圆 xx 公里内的成员georadiusbymember cities:locations beijing90km3.4 逆地理编码经纬度转地址拿到经纬度后可以借助第三方 API百度、高德或geopy反向解析出具体地址fromgeopy.geocodersimportNominatim geolocatorNominatim(user_agentmy_application)defgeolocate_point(latitude,longitude):locationgeolocator.reverse(f{latitude},{longitude})returnlocation.address latitude39.983424longitude116.307224print(geolocate_point(latitude,longitude))四、持久化4.1 什么是持久化Redis 的所有数据保存在内存中对数据的更新会异步地保存到硬盘上从而做到持久存储即便断电或重启数据也不会丢失。持久化的两种实现思路快照某一时刻数据的完整备份。例如 MySQL 的 Dump、Redis 的 RDB。写日志把每一次操作记录下来恢复时重放日志即可。例如 MySQL 的 Binlog、Redis 的 AOF。Redis 共提供三种持久化方案RDB快照持久化AOF日志持久化混合持久化RDB AOF 结合。4.2 RDB 快照RDB 通过在某一时刻把内存数据保存到硬盘生成.rdb文件实现持久化触发方式有三种人工同步save——同步操作会阻塞其他命令Redis 单线程架构人工异步bgsave——异步操作不会阻塞其他命令配置文件自动触发满足条件时由后台自动保存。# 配置文件触发规则save 时间间隔秒数 修改次数save9001# 900 秒内至少有 1 次修改save30010# 300 秒内至少有 10 次修改save6010000# 60 秒内至少有 10000 次修改最佳配置建议save9001save30010save6010000dbfilename dump-6379.rdb# 用端口号命名避免同一机器多实例文件名冲突dir./bigdiskpath# 保存到大硬盘目录stop-writes-on-bgsave-error yes# 保存出错时停止写入rdbcompression yes# 开启压缩rdbchecksum yes# 开启校验RDB 的问题耗时、耗性能且两次快照之间的数据可能丢失恢复可控性较差。RDB 比较适合用于缓存场景的数据备份。4.3 AOF 日志AOF 会把客户端每一条写命令都追加到日志文件宕机后可以通过重放日志完全恢复数据。日志不会直接写进磁盘而是先进入缓冲区再由策略决定何时刷盘。三种刷盘策略always每条命令都fsync到硬盘最安全但性能最低everysec默认值每秒把缓冲区fsync一次兼顾安全与性能no由操作系统决定何时刷盘性能最高但可能丢失较多数据。appendonly yes# 开启 AOFappendfilename appendonly-6379.aof# 文件名appendfsync everysec# 刷盘策略dir/bigdiskpath# 存放路径no-appendfsync-on-rewrite yes# 重写期间不追加日志AOF 相关文件appendonly.aof.1.base.rdb永久保存的基准文件appendonly.aof.1.incr.aof临时增量日志appendonly.aof.manifest记录哪些文件包含了完整数据。4.4 AOF 重写随着运行时间变长AOF 文件会越来越大宕机重启时重演整个文件会非常耗时。AOF 重写就是给日志瘦身把失效的、可合并的日志合并使新文件记录相同的数据但体积更小。重写的特点新旧 AOF 保存的数据库数据完全一致新文件用更少的命令记录体积更小重写期间服务器不阻塞可正常处理命令即使重写失败也不会丢数据旧文件在成功前不会被修改。触发方式手动BGREWRITEAOF自动配置auto-aof-rewrite-percentage100auto-aof-rewrite-min-size 64mb# 达到 64MB 后文件增量超过 100% 才触发重写即第一次重写时文件为 64M则第二次触发阈值为 128M第三次为 256M以此类推。将百分比设为 0 表示关闭自动重写。新旧版本差异老版本重写后优化日志文件仍是日志形式新版本重写时把此刻内存数据以 RDB 形式放入aof-base后续新增日志放入aof-incr。即aof-base是 RDB 二进制aof-incr是日志形式。4.5 混合持久化7.x 默认自动开启混合持久化为了加快恢复速度。开启后AOF 重写时会先以 RDB 格式把当前数据写入 AOF 文件开头之后的新增数据再以 AOF 日志格式追加到末尾。appendonly yes appendfilenameappendonly-6379.aofappendfsync everysec no-appendfsync-on-rewrite yes aof-use-rdb-preamble yes# 开启混合持久化使用混合持久化的要点同时开启 RDB 与 AOF并设置aof-use-rdb-preamble yes。触发 AOF 重写时Redis 会把 RDB 二进制放到 AOF 文件开头。AOF 各文件作用总结xx.manifest记录哪些 AOF / RDB 文件合并起来才是完整数据xx.2.incr.aof存放日志无 RDB 二进制xx.aof.2.base.rdb存放 RDB 二进制文件名中的数字表示重写了第几次。4.6 三种方案对比方案实现方式优点缺点适用场景RDB快照文件紧凑、恢复快、适合备份两次快照间可能丢数据、save 阻塞缓存、冷备AOF日志更安全、几乎不丢数据文件大、恢复慢对数据完整性要求高的场景混合RDB AOF体积小且恢复快需要 Redis 4.0默认推荐方案五、主从复制5.1 概念与作用主从复制让从库复制主库的数据保证主从数据一致从而解决三类问题机器故障靠主从 哨兵容量瓶颈靠集群QPS并发量瓶颈靠主从做读写分离。架构上支持一主一从、一主多从。作用有两个读写分离主库写、从库读数据副本多一份数据做备份。注意事项一个 master 可以有多个 slave一个 slave 只能有一个 master数据流向是单向的从 master 到 slave。5.2 复制流程从库通过slaveof 主库IP 端口连接主库并发送SYNC主库收到 SYNC 后触发BGSAVE后台保存 RDB 并发送给从库从库接收并应用 RDB 快照期间产生的新操作会陆续保存并发送给从库从此刻起主从复制正常工作之后主库的每次新操作都以命令传播形式自动同步给从库所有复制相关信息都能在info中查到重启后主从关系依然保留若主从断开从库数据不会损坏重连后从库发送PSYNC主库只把从库缺失的部分同步过去实现快速恢复。主库是否开启持久化不是强制要求但为防止数据丢失通常都会开启。5.3 实操命令方式在 6380 的从库上执行# 建立主从slaveof10.0.0.1016379# 断开主从slaveof no one配置文件方式slaveof10.0.0.1016379slave-read-only yes# masterauth 654321 # 主库有密码时必须配置通过info replication可查看主从信息。5.4 辅助配置min-slaves-to-write2min-slaves-max-lag3当从库数量少于 2 个或从库延迟lag都大于等于 3 秒时主库将拒绝执行写命令用于保障数据一致性。5.5 Python 与 Django 操作redis-py 原生操作自己处理主从conn1链接主库 conn2链接从库 conn1.set(name,zzz)# 写主库print(conn2.get(name))# 读从库Django 缓存配置配合 django-redisCACHES{default:{BACKEND:django_redis.cache.RedisCache,LOCATION:redis://localhost:6379/1,OPTIONS:{CLIENT_CLASS:django_redis.client.DefaultClient},},redis1:{BACKEND:django_redis.cache.RedisCache,LOCATION:redis://localhost:6380/0,OPTIONS:{CLIENT_CLASS:django_redis.client.DefaultClient},},}fromdjango.core.cacheimportcaches caches[redis1].set(name,lqz)# 写rescaches[default].get(name)# 读主从复制时若主库有密码从库需要配置masterauth。六、哨兵Sentinel6.1 作用哨兵sentinel用于保障 Redis 的高可用。在一主多从环境下如果主库挂掉哨兵会自动选出一个从库升级为主库其他从库复制这个新主库之前宕机的主库重新上线后会自动作为从库加入。它实际上是一个独立的进程客户端直接连接哨兵的地址即可。哨兵解决的问题主从复制下主节点故障需要手动转移——靠哨兵自动完成故障转移主从只能主写写能力和存储能力有限——靠集群解决。6.2 哨兵原理哨兵可以完成故障判断、故障转移、通知客户端三件事流程如下多个 sentinel 发现并确认 master 有问题选举出一个 sentinel 作为领导者选取一个 slave 作为新的 master通知其余 slave 复制新的 master通知客户端主从关系已变化等待旧 master 复活后作为新 master 的 slave 重新加入。6.3 搭建步骤一主两从配置三台机器需互相联通主库daemonize yes bind0.0.0.0pidfile/var/run/redis.pidport6379dir/usr/local/bin/datalogfile6379.logprotected-mode no requirepass654321save605appendonly yes appendfilenameappendonly-6379.aofappendfsync everysec no-appendfsync-on-rewrite yes aof-use-rdb-preamble yes从库 1端口 6380daemonize yes bind0.0.0.0port6380dir/usr/local/bin/data1logfile6380.logprotected-mode no requirepass654321save605appendonly yes appendfilenameappendonly-6380.aofappendfsync everysec no-appendfsync-on-rewrite yes aof-use-rdb-preamble yes slaveof10.0.0.1016379masterauth654321slave-read-only yes从库 2端口 6381与从库 1 类似端口改为 6381、目录改为data2、日志改为6381.log、文件名改为appendonly-6381.aof。三个哨兵配置文件端口分别为 26379、26380、26381port26379daemonize yesdirdata protected-mode no bind0.0.0.0logfileredis_sentinel.logsentinel monitor mymaster127.0.0.163792sentinel auth-passmymaster654321sentinel down-after-milliseconds mymaster30000sentinel parallel-syncs mymaster1sentinel failover-timeout mymaster180000启动三个哨兵redis-sentinel./sentinel_26379.conf redis-sentinel./sentinel_26380.conf redis-sentinel./sentinel_26381.conf连接哨兵并查看状态redis-cli-p26379info此时停止主库哨兵会自动完成故障转移。6.4 Python 操作哨兵importredisfromredis.sentinelimportSentinel# 连接哨兵集群sentinelSentinel([(10.0.0.101,26379),(10.0.0.101,26380),(10.0.0.101,26381)],socket_timeout5,)# master sentinel.discover_master(mymaster)# slave sentinel.discover_slaves(mymaster)# 往主库写# master sentinel.master_for(mymaster, socket_timeout0.5, password654321)# master.set(foo, bar)# 从从库读slavesentinel.slave_for(mymaster,socket_timeout0.5,password654321)print(slave.client().connection_pool)r_retslave.get(foo)print(r_ret)七、Redis 集群Cluster7.1 为什么需要集群单机 Redis 存在两个瓶颈并发量单机 QPS 约 10 万/s而业务可能需要百万级别数据量单机内存有限16G~256G可能存不下 500G 数据。集群通过数据分布分布式数据库来解决。7.2 分区方式对比分区方式特点代表哈希分布数据分散度高、键值与业务无关、无法顺序访问一致性哈希 memcache、Redis Cluster顺序分布数据分散度易倾斜、键值业务相关、可顺序访问BigTable、HBase顺序分区把 100 条数据按区间分到 3 个节点如 1~33 在节点一、34~66 在节点二、67~100 在节点三关系型数据库常用。哈希分区的三种细分节点取余分区key 经哈希后对节点数取余决定归属。扩容时需要迁移大量数据推荐翻倍扩容以减少迁移量。一致性哈希分区客户端分片采用哈希 顺时针方式。节点伸缩只影响临近节点但仍可能迁移数据无法保证负载均衡翻倍扩容可实现均衡。虚拟槽分区Redis 采用预设虚拟槽每个槽映射一个数据子集槽数量一般远大于节点数配合 CRC16 等哈希函数。Redis 槽范围 0~16383。Redis Cluster 的工作原理5 个节点把 16384 个槽均分客户端把数据发给任意节点节点用CRC16(key) mod 16383算出 key 属于哪个槽从而确定目标节点。每个节点都记录自己负责的槽若槽不在自己范围内节点间通过共享消息知道谁负责该槽返回结果让客户端去对应节点存取。7.3 为什么槽是 16384 个Redis 中HASH_SLOT CRC16(key) mod 16384。CRC16 理论上最多产生 65535 个槽但 Redis 取 16384原因有二节点发心跳包时要携带全部槽位16384 个槽占16384/8 2KB若用 65536 个槽则占 8KB心跳包过大。Redis Cluster 不太可能扩展到超过 1000 个节点太多会网络拥堵16384 足够每个主节点分到足够多的槽。7.4 搭建一个 3 主 3 从集群步骤概览编写 6 个配置文件 → 启动 6 个节点 → 用redis-cli --cluster create创建集群 → 测试读写 → 验证故障转移。# 第一步写 6 个 redis 配置文件以 7000 为例port7000daemonize yesdir/root/redis/data/logfile7000.logdbfilenamedump-7000.rdbcluster-enabled yes cluster-config-filenodes-7000.conf cluster-require-full-coverage yes# 第二步复制生成剩余 5 个配置seds/7000/7001/gredis-7000.confredis-7001.conf seds/7000/7002/gredis-7000.confredis-7002.conf seds/7000/7003/gredis-7000.confredis-7003.conf seds/7000/7004/gredis-7000.confredis-7004.conf seds/7000/7005/gredis-7000.confredis-7005.conf# 第三步启动 6 个节点redis-server./redis-7000.conf redis-server./redis-7001.conf redis-server./redis-7002.conf redis-server./redis-7003.conf redis-server./redis-7004.conf redis-server./redis-7005.conf# 查看集群信息cluster info cluster nodes# 第四步创建集群--cluster-replicas 1 表示每个主节点配 1 个从节点redis-cli--cluster create--cluster-replicas1127.0.0.1:7000127.0.0.1:7001127.0.0.1:7002127.0.0.1:7003127.0.0.1:7004127.0.0.1:7005# 输入 yes 确认# 第五步测试# 往 7000 写 set name lqz 可能写不进去因为 name 哈希后的槽不归 70000-5460 槽管# 会返回错误并提示应写入对应节点换到正确节点即可写入。# 第六步查看节点信息cluster nodes cluster info# 集群模式连接自动切换节点redis-cli-p7000-c# 第七步停掉一个主节点其从节点会自动升级为主节点故障转移Python 操作集群需要pip3 install redis-py-clusterfromredisclusterimportRedisCluster startup_nodes[{host:127.0.0.1,port:7005},{host:127.0.0.1,port:7001},{host:127.0.0.1,port:7002},]rcRedisCluster(startup_nodesstartup_nodes)rc.set(foo,bar)print(rc.get(foo))7.5 扩容新增两个节点7006、7007让它们加入集群并迁移槽# 1 准备两台机器seds/7000/7006/gredis-7000.confredis-7006.conf seds/7000/7007/gredis-7000.confredis-7007.conf# 2 启动redis-server./redis-7006.conf redis-server./redis-7007.conf# 3 加入集群redis-cli--cluster add-node127.0.0.1:7006127.0.0.1:7000redis-cli--cluster add-node127.0.0.1:7007127.0.0.1:7000# 4 让 7007 复制 7006成为其从节点redis-cli-p7007cluster replicate 2bab67072c1b1f95cf6e3cb79e8cd29cd5b147f9# 5 迁移槽4096 个槽 16384 / 4redis-cli--cluster reshard127.0.0.1:7000# 迁移 4096 个槽指定 7006 接收选择 all7.6 缩容下线 7006 及其从节点需先把 7006 上的槽迁回其他节点# 第一步把 7006 的槽迁移到 7000/7001/7002redis-cli--cluster reshard--cluster-from2bab67072c1b1f95cf6e3cb79e8cd29cd5b147f9--cluster-to 65cc604cdcb4ecf1566dc1182e6f25b276589d20--cluster-slots1365127.0.0.1:7000redis-cli--cluster reshard--cluster-from2bab67072c1b1f95cf6e3cb79e8cd29cd5b147f9--cluster-to 25652d07d3da463255f6f794b61f156e8015f6cb--cluster-slots1366127.0.0.1:7001redis-cli--cluster reshard--cluster-from2bab67072c1b1f95cf6e3cb79e8cd29cd5b147f9--cluster-to de4ad907904bda3c12ddadbf69533d34a0ff31f2--cluster-slots1365127.0.0.1:7002# 第二步忘记节点先下从、再下主避免故障转移redis-cli--clusterdel-node127.0.0.1:70002bab67072c1b1f95cf6e3cb79e8cd29cd5b147f9 redis-cli--clusterdel-node127.0.0.1:7000842d379b428100880ed5414cd9816df2cda02d3a# 第三步关掉其中一个主节点其从节点顶上重启被停的节点后它会作为从节点八、缓存优化8.1 缓存更新策略当内存达到上限时可通过maxmemory-policy选择淘汰策略LRULeast Recently Used剔除最长时间未被使用的数据LFULeast Frequently Used剔除一定时间段内使用次数最少的数据FIFOFirst In First Out先进先出剔除最早进入的数据。8.2 缓存穿透、击穿、雪崩缓存穿透缓存和数据库中都没有的数据被用户可能为攻击者不断请求导致数据库压力过大。解决方案接口层增加校验例如用户鉴权、id 基础校验id 0直接拦截缓存取不到且数据库也没有时写入key - null并设置较短的过期时间如 30 秒防止同一 id 被反复攻击使用布隆过滤器拦截不存在的 key。缓存击穿缓存中没有但数据库中有一般是缓存刚过期大量并发同时读缓存未命中又同时查数据库导致数据库压力瞬间增大。它与穿透的区别击穿是同一条热点数据。解决方案设置热点数据永不过期对数据库查询加互斥锁让并发请求只有一个去查询数据库。缓存雪崩缓存中大量数据同时过期导致大量请求直接打到数据库甚至让数据库宕机。与击穿不同雪崩是不同数据同时过期。解决方案缓存过期时间设置随机值避免大量数据同一时间过期分布式部署时把热点数据均匀分布到不同缓存节点热点数据设置永不过期。补充有序集合ZSet底层使用了跳跃表数据结构这在排行榜等高并发场景中非常关键相关内容已在 Day 03 详细讲述。
返回列表