Redis HA部署&使用

Redis广泛应用于各种项目中,尤其是锁和Celery等应用场景。本文主要介绍Redis的HA部署&使用,如果没有水平扩展(大数据)的需求,建议使用哨兵模式。

HA部署模式

Redis主要有以下三种部署模式

  1. 主从复制(Master-Slave Replication)

    当主节点失效时,可以手动或通过某种自动方式将其中一个从节点提升为新的主节点。

  2. 哨兵模式(Sentinel)
    • 基于主从复制,增加了监控、通知和自动故障转移功能。
    • 当主节点失效时,哨兵会自动从现有的从节点中选举一个新的主节点
    • 主从节点都可以读取,但只有主节点可以写入
    • 最少2个Redis实例,3个Sentinel实例(选举)以保证其可用性和决策的正确性
    • 客户端先访问哨兵获取最新的master节点,然后再进行操作

  3. 集群模式(Cluster)
    • Redis Cluster 是 Redis 的分布式解决方案,提供了数据分片和高可用性。
    • 每个分片都有一个主节点和0个或多个从节点。
    • 主节点负责分片中的一部分键值对,从节点为其对应的主节点提供复制和故障恢复。
    • 自动重定向,访问一个不在当前节点上的键,Redis会返回一个“MOVED”错误,告诉客户端正确的节点地址和端口(主节点),由客户端处理(透明)
    • 主从节点都可以读取,但只有主节点可以写入
    • 至少需要6个节点(3主 + 3从)
      • 通过“哈希插槽”技术将数据分散在各个节点上。总共有16384个哈希插槽插槽
      • 在最小集群配置中,会被均匀地分配给3个主节点。每个主节点负责大约5461个插槽

哨兵 VS 集群

*哨兵Sentinel *集群Cluster
部署
目标 高可用性 高可用性+水平扩展(大数据)
节点数 3+(3+2) 6+(3+3)
部署复杂性 简单 复杂
水平扩展性 不可以(全量数据) 可以(数据分片)
从节点是否可写
开发
客户端 和哨兵返回的master节点交互 和任意节点交互,根据 `MOVED` 或 `ASK` 重定向到目标节点
Python 支持 支持
Celery Broker 支持 不支持(6.0 Milestone)
Key是否需要额外处理 是(Hash Tag解决批量操作问题)
Python+Redis+Websocket 支持 不支持(AttributeError: 'RedisCluster' object has no attribute 'pubsub')
功能限制 1.BY option of SORT denied in Cluster mode

Cluster Hash Tag

Redis Cluster 使用一种散列技术将数据划分到不同的节点(shards)上。默认情况下,Redis 使用键的 CRC16 值的一部分(具体地说,是 CRC16 值的最后 4 位)来决定将其存储在哪个 slot 上。因为 Redis Cluster 有 16384 个 slot,所以这种方法可以均匀地将数据分布在不同的节点上。 但是,有时您可能希望确保某些键始终在同一个 slot(因此在同一个节点)上。这对于那些需要执行多键操作(例如 `MSET` 或 `MGET`)的键尤其有用,并且希望保证它们在同一个节点上。 Cluster hash tags 是 Redis Cluster 中的一个特性,允许您通过在键名中加入 `{...}` 来覆盖默认的分片行为。具体地说,如果键名中包含 `{...}`,则只有 `{...}` 内的部分用于计算 slot

                
                `user:{123}:name`	-123用于计算slog
                `user:{123}:age`	-123用于计算slog

                `order:{456}:total`		-456用于计算slog
                `order:{456}:status`	-456用于计算slog
                
            

如果一个键包含了多个`{...}`部分,只有**第一个**`{...}`部分会被考虑。例如,在`keyname:{hashtag1}:{hashtag2}`中,只有`hashtag1`被用来计算哈希slot。redisz一次操作多个键值,也需要使用Hash Tag

                
                    rdz.rename('test:{i}0', 'test:{i}1')
                    rdz.list_bpop(['test:{numbers}1', 'test:{numbers}2']
                
            

Redis in Celery(哨兵模式)

                
                    CELERY = {
                        'broker_url': 'sentinel://10.124.206.61:27001;10.124.206.62:27001;10.124.206.63:27001', # Broker Redis哨兵列表
                        'broker_transport_options': {
                            'master_name': 'mymaster',
                            'visibility_timeout': 3600,
                            'socket_timeout': 10,
                        },
                        'result_backend': 'sentinel://10.124.206.61:27001;10.124.206.62:27001;10.124.206.63:27001', # Result Redis哨兵列表
                        'result_backend_transport_options': {
                            'master_name': 'mymaster',
                            'visibility_timeout': 3600,
                            'socket_timeout': 10,
                        },
                        # ...
                    }
                
            

Redis +Websocket(哨兵模式)

                
                    # ws_redis = redis.asyncio.from_url(ws_config.get('REDIS_URL'))  # 单点
                    sentinel = redis.asyncio.Sentinel([("10.124.206.61", 27001), ("10.124.206.62", 27001), ("10.124.206.63", 27001)])	# 哨兵
                    ws_redis = sentinel.master_for("mymaster")
                    ws_channel = ws_redis.pubsub()

                    flaskz_logger.debug('redis websocket channel=' + str(channel) + ' connect')
                    await ws_channel.subscribe(*channel)  # 每个ws连接监听的channel可以不一样
                    try:
                        while True:
                            if websocket.closed:
                                break
                            redis_message = await ws_channel.get_message(ignore_subscribe_messages=True, timeout=timeout)
                            if redis_message:
                                redis_message = redis_message.get('data')
                                await websocket.send(redis_message.decode('utf-8'))
                    except websockets.WebSocketException as e:
                        flaskz_logger.debug('redis-ws websocket exception= ' + str(e))
                    except redis.exceptions.ConnectionError as e:  # @2023-09-01 添加redis哨兵模式的逻辑处理
                        flaskz_logger.debug('redis-ws redis connection error= ' + str(e))
                        await ws_channel.unsubscribe(channel)
                        await websocket_handler(websocket, path)  # reconnect redis
                    finally:
                        await ws_channel.unsubscribe(channel)
                        flaskz_logger.debug('redis websocket channel=' + str(channel) + ' clear resource')
                
            

安装&配置

哨兵模式

  1. 修改redis.conf配置
                            
                                bind 0.0.0.0	# 绑定地址
                                port 6379			# 端口
                                databases 16	# 数据库数量,默认为16
                                daemonize yes	# 是否后台运行 yes/no
    
                                # 自动创建快照条件/满足一个即创建快照,注释掉所有的 save 配置行,并设置 save ""以禁用快照持久化
                                save 3600 1
                                save 300 100 	# 300s内有100个键值变化
                                save 60 10000
    
                                # *仅Slave节点配置,启动时手动设置,后由哨兵维护
                                # slaveof 10.124.206.61 6379
                            
                        
  2. 修改哨兵sentinel.conf配置
                            
                                port 26379																			# 哨兵端口,默认=26379
                                sentinel announce-ip "10.124.206.61"  					# 哨兵地址(所在节点IP地址)
                                sentinel monitor mymaster 10.124.206.61 6379 2 	# Redis master地址,最小必要数量quorum= n/2+1=2
                                sentinel down-after-milliseconds mymaster 5000	# Redis master 5000毫秒不响应,则认为master不可达
                                sentinel failover-timeout mymaster 60000				# 如果故障转移在60000毫秒内没有完成,则视为失败
                                dir "/usr/local/redis/sentinel"									# Sentinel元数据的存储目录,元数据可能包括哪个主服务器被认为是不可达的、选举的状态等
                                daemonize yes																		# 是否后台运行 yes/no
    
                                # Generated by CONFIG REWRITE(自动生成)
                                latency-tracking-info-percentiles 50 99 99.9
                                protected-mode no
                                user default on nopass sanitize-payload ~* &* +@all
                                sentinel myid f0d4d469ca27d91fc3822bd72b73830615c812de
                                sentinel config-epoch mymaster 7
                                sentinel leader-epoch mymaster 7
                                sentinel current-epoch 7
    
                                sentinel known-sentinel mymaster 10.124.206.62 26379 c78f30077b1d733e2bdfa2b2d5149a16bc6b9ca4	# 其他哨兵1
    
                                sentinel known-sentinel mymaster 10.124.206.63 26379 46f8504b1d74c60094b0f59bd3b5302933c2e91c	# 其他哨兵2
    
                                sentinel known-replica mymaster 10.124.206.63 6379	# Redis slave节点1
    
                                pidfile "/var/run/redis.pid"
    
                                sentinel known-replica mymaster 10.124.206.62 6379	# Redis slave节点2
                            
                        
  3. 启动redis
                                
                                    redis-server /path/to/redis.conf
                                
                        
  4. 启动哨兵
                            
                                redis-sentinel /path/to/sentinel.conf
                            
                        

集群模式

  1. 修改redis.conf配置
                            
                                bind 0.0.0.0		# 绑定地址
                                port 6379				# 端口
                                databases 16		# 数据库数量,默认为16
                                daemonize yes		# 是否后台运行 yes/no
    
                                # 自动创建快照条件/满足一个即创建快照,注释掉所有的 save 配置行,并设置 save ""以禁用快照持久化
                                save 3600 1
                                save 300 100 	# 300s内有100个键值变化
                                save 60 10000
    
                                cluster-enabled yes		# 开启集群功能
                                cluster-config-file nodes.conf	# 集群配置文件,用于保存集群的状态和配置(Redis自动维护)
                                cluster-node-timeout 5000# 节点被认为不可达之前等待的毫秒数
                            
                        
  2. 启动redis
                            
                                redis-server /path/to/redis.conf
                            
                        
  3. 初始化集群

Supervisor

                
                    [program:redis-server]
                    command=/path/to/redis-server /path/to/redis.conf
                    autostart=true
                    autorestart=true
                    stderr_logfile=/var/log/redis/redis.err.log
                    stdout_logfile=/var/log/redis/redis.out.log