Persistence internals
Redis keeps its working data in memory. If the process exits and nothing has been saved elsewhere, that data disappears permanently. Persistence is the mechanism that periodically writes the in-memory state to disk so Redis can restore it after a restart.
A write that finally lands on disk usually travels through several layers:
- A client sends a write request to the Redis server.
- Redis receives the write data in the server process.
- The server calls the
writesystem call and writes data toward disk. - The operating system moves data from its buffer to the disk controller.
- The disk controller writes the data to the physical storage medium.
The propagation path can be viewed as:
client memory -> server memory -> OS memory buffer -> disk buffer -> disk
If only the Redis process fails, data is generally safe once the third step has completed, because the remaining flushing work can still be handled by the operating system. If the operating system itself fails, all five steps must have completed for the data to be durable.
Redis provides two main persistence mechanisms for this problem: RDB, short for Redis DataBase, and AOF, short for Append Only File.
RDB: full snapshot persistence
RDB is Redis's default full-snapshot persistence strategy. It periodically creates a snapshot of the current process data and saves it to disk as a file with the .rdb suffix. When Redis restarts, it can load this snapshot file to recover data.
RDB-related settings are configured in redis.conf.
How RDB is triggered

The SAVE command can manually trigger RDB generation. This blocks the Redis server until the RDB file has been created, so it should not be used in production environments.

The BGSAVE command is the more practical method. Redis forks a child process, and the child process performs the persistence work. The main blocking point is only the fork itself.
A few details matter here:
- The child process does not copy all memory data directly. It copies the parent's page table, while still sharing the same physical memory pages with the parent process.
- During
bgsave, the child process writes a temporary RDB file first instead of modifying the existing RDB file directly. - Redis uses copy-on-write to prevent dirty writes.
- If the main process only reads shared memory, no copy is made.
- If the main process writes to a shared memory page, the operating system copies that page. The main process writes to the new copy, while the child process continues reading the old version.
Besides manual commands, Redis can also trigger RDB automatically:
save x yinredis.confmeans: if at leastykeys change withinxseconds, triggerbgsave.- During master-replica synchronization, when a replica requests synchronization, the master triggers
bgsaveand sends the generated snapshot to the replica. - Running
DEBUG RELOADto reload Redis also triggersbgsave. - By default, when
shutdownis executed and AOF persistence is not enabled, Redis also triggersbgsave.
AOF: command log persistence
AOF is not enabled by default. Instead of saving the entire dataset as a snapshot, it records user write commands as a log. Read operations are not recorded because they do not change stored data.
AOF has several basic characteristics:
- It records write requests in log form.
- It appends to the file rather than modifying previous content.
- Recovery is performed by reading the AOF file from beginning to end and replaying the write commands.
AOF persistence flow

AOF persistence consists of four main stages:
- Command append: write commands are appended to the AOF buffer.
- File synchronization: the AOF buffer is synchronized to disk according to the configured strategy.
- File rewrite: as appended commands accumulate, the AOF file grows. Redis rewrites it to reduce file size.
- Restart recovery: after Redis restarts, it reloads the AOF file and re-executes commands to restore data.
AOF can be enabled in redis.conf:
appendonly yes
AOF flush frequency has three common modes:
always: flush to disk after every write. Reliability is high, but performance is heavily affected.everysec: flush once per second. This is a balanced option and may lose up to one second of data.no: let the operating system decide when to flush. Performance is best, but reliability is poor and more data may be lost.
AOF rewriting can be triggered with:
bgrewriteaof
Redis can also be configured to trigger rewriting automatically after the file grows beyond a threshold.
AOF recovery
Redis restores state by replaying the commands in the AOF file, but it does not do this through a normal network client.

The process is:
- Redis creates a fake client without a network connection. Since recovery only needs to load the AOF file, no actual network connection is needed.
- Redis reads one command from the AOF file and executes it using the fake client.
- This repeats until all commands in the AOF file have been executed.
AOF rewrite
Because AOF keeps appending commands, the file can become excessively large. Redis therefore provides AOF rewrite.

For example, before rewriting, multiple commands may be needed to describe the current state of a list key. After rewriting, Redis can record the same final state with fewer commands. The result is equivalent, but the file is smaller.
Important points:
- AOF rewrite does not read, analyze, and modify the existing AOF file.
- It reads the current database state from the Redis server and generates a new command sequence from that state.
- For a key, Redis reads the current value and writes a minimal command representation that can recreate that value, replacing the many historical commands that produced it.
Rewrite triggers include:
- Manual trigger:
bgrewriteaof. If a rewrite child process is already running, the new rewrite is delayed; otherwise it starts immediately. - Automatic trigger: controlled by settings such as
auto-aof-rewrite-min-size 64mb; when the AOF file exceeds the configured size, Redis can start rewriting automatically.
Why AOF rewrite uses a child process
AOF rewrite performs many write operations. If the main thread executed it directly, Redis would be blocked for a long time. Therefore Redis performs AOF rewrite in a child process.

During the entire rewrite process, only signal handling causes blocking in the main Redis process. At other times, the main process continues serving requests.

RDB and AOF priority
RDB and AOF differ in several practical ways:
- File size: RDB files are compressed by default using the LZF algorithm, so their size is usually much smaller than the memory footprint. They are suitable for backups and full replication.
- Recovery speed: Redis loads RDB files much faster than AOF files, because AOF recovery must execute all recorded commands.
- Memory cost: both mechanisms need to fork a child process. Forking is a heavyweight operation, and doing it too frequently is expensive.
- Backup and disaster recovery: RDB files are binary and not readable. AOF files can be manually inspected, repaired, or completed if their format is understood.
- Data consistency: RDB is not real-time enough and cannot guarantee second-level persistence. AOF can limit data loss to at most about one second when using
everysec.
If both RDB and AOF are enabled, Redis gives priority to AOF during restart recovery. If no AOF file exists, it falls back to RDB recovery.
Hybrid RDB-AOF persistence
Both RDB snapshots and AOF rewriting require Redis to fork a child process, which can temporarily block the main process. Common mitigation approaches include:
- Reduce fork frequency by manually controlling RDB snapshots or AOF rewrites.
- Limit Redis memory usage to prevent
forkfrom taking too long. - Configure Linux memory allocation policies properly to avoid fork failures caused by insufficient physical memory.
In production, if data is not especially sensitive or can be regenerated, persistence can also be disabled.
For other scenarios, Redis 4.0 introduced hybrid persistence: combining AOF logs with memory snapshots.
Enable it in redis.conf:
aof-use-rdb-preamble yes
When hybrid persistence is enabled, AOF rewrite works like this: the forked child process first writes the memory data shared with the main thread into the AOF file in RDB format. Meanwhile, write commands handled by the main thread are recorded into the rewrite buffer. These incremental commands are then appended to the AOF file in AOF format. After writing finishes, Redis notifies the main process to replace the old AOF file with the new file containing both RDB and AOF sections.
For a hybrid AOF file, the first part is full data in RDB format, and the second part is incremental data in AOF format.
This improves recovery speed because Redis can quickly load the RDB portion first, then replay the later AOF commands to reduce the risk of data loss.
Security strategy
Password authentication
Redis can require clients to authenticate with a password. This is configured in redis.conf through requirepass.
A common password requirement is: at least 8 characters, including three of the following four types: uppercase letters, lowercase letters, digits, and symbols.
After configuration, Redis must be restarted for the file-based setting to take effect.
The password can also be changed by command:
CONFIG SET requirepass password- Login requires:
AUTH password
Notes:
- This password is the password of the
defaultuser. - In
initServer, Redis callsACLUpdateDefaultUserPassword(server.requirepass)to set the password of the default user.
/* Set the password for the "default" ACL user. This implements supports for
* requirepass config, so passing in NULL will set the user to be nopass. */
void ACLUpdateDefaultUserPassword(sds password) {
ACLSetUser(DefaultUser,"resetpass",-1);
if (password) {
sds aclop = sdscatlen(sdsnew(">"), password, sdslen(password));
ACLSetUser(DefaultUser,aclop,sdslen(aclop));
sdsfree(aclop);
} else {
ACLSetUser(DefaultUser,"nopass",-1);
}
}
When the password does not take effect
The Redis configuration contains the following note:
# IMPORTANT NOTE: starting with Redis 6 "requirepass" is just a compatibility
# layer on top of the new ACL system. The option effect will be just setting
# the password for the default user. Clients will still authenticate using
# AUTH <password> as usually, or more explicitly with AUTH default <password>
# if they follow the new protocol: both will work.
#
# The requirepass is not compatible with aclfile option and the ACL LOAD
# command, these will cause requirepass to be ignored.
#
# requirepass foobared
Starting with Redis 6.0, requirepass is only a compatibility layer over the ACL system and only configures the default user. After Redis loads the configuration, if it then reads aclfile, it recreates the global Users object. This calls ACLInitDefaultUser, which recreates a nopass default user. As a result, the configured requirepass may become ineffective.
Solutions:
- Start Redis without reading
aclfile, for example:redis-server ./redis.conf. - If
aclfileis enabled, log in withredis-cli, runconfig set requirepass xxx, and then runacl save. This writes the default user's rule into the ACL file.
Expiration strategy
Basic expiration commands
Redis keys can have expiration times. Common commands include:
expire <key> <n>: expire the key afternseconds.pexpire <key> <n>: expire the key afternmilliseconds.expireat <key> <n>: expire the key after the specified Unix timestamp, accurate to seconds.pexpireat <key> <n>: expire the key after the specified Unix timestamp, accurate to milliseconds.
Expiration can also be set when creating the key:
set <key> <value> ex <n>: set a value and expiration in seconds.set <key> <value> px <n>: set a value and expiration in milliseconds.setex <key> <n> <va1ue>: set a value and expiration in seconds as an atomic operation.
Related commands:
persist <key>: remove the key's expiration time.TTL <key>: return remaining lifetime in seconds.PTTL <key>: return remaining lifetime in milliseconds.
How Redis deletes expired keys
Before deleting an expired key, Redis must determine whether the key is expired.
Internally, when an expiration is set for a key, Redis stores that expiration time in an expiration dictionary inside redisDb. Each time a key is queried, Redis first checks whether the key exists in the expiration dictionary:
- If it does not exist, Redis returns normally.
- If it exists, Redis compares the stored expiration time with the current system time.
- If the key is expired, Redis handles it according to the expiration deletion strategy.
There are three conceptual expiration strategies:
- Lazy deletion: Redis checks whether a key is expired only when a client tries to access it. If expired, it deletes it.
- Timed deletion: when setting an expiration time, Redis creates a timer and deletes the key immediately when the time arrives.
- Periodic deletion: Redis periodically samples a batch of keys, checks them, and deletes expired ones.
Redis uses lazy deletion plus periodic deletion.
Lazy deletion
In db.c, Redis repeatedly calls expireIfNeeded to check whether a key should expire.
/* Return values for expireIfNeeded */
typedef enum {
KEY_VALID = 0, /* Could be volatile and not yet expired, non-volatile, or even non-existing key. */
KEY_EXPIRED, /* Logically expired but not yet deleted. */
KEY_DELETED /* The key was deleted now. */
} keyStatus;
keyStatus expireIfNeeded(redisDb *db, robj *key, int flags) {
if (server.lazy_expire_disabled) return KEY_VALID; // 未设置过期策略直接返回 key 值
if (!keyIsExpired(db,key)) return KEY_VALID;
/* If we are running in the context of a replica, instead of
* evicting the expired key from the database, we return ASAP:
* the replica key expiration is controlled by the master that will
* send us synthesized DEL operations for expired keys. The
* exception is when write operations are performed on writable
* replicas.
*
* Still we try to return the right information to the caller,
* that is, KEY_VALID if we think the key should still be valid,
* KEY_EXPIRED if we think the key is expired but don't want to delete it at this time.
*
* When replicating commands from the master, keys are never considered
* expired. */
// 这里说明了,从节点的 key 过期策略是由主节点控制的,如果是在复制主节点的命令时,键永远不会被视为已过期
if (server.masterhost != NULL) {
if (server.current_client && (server.current_client->flags & CLIENT_MASTER)) return KEY_VALID;
if (!(flags & EXPIRE_FORCE_DELETE_EXPIRED)) return KEY_EXPIRED;
}
/* In some cases we're explicitly instructed to return an indication of a
* missing key without actually deleting it, even on masters. */
if (flags & EXPIRE_AVOID_DELETE_EXPIRED)
return KEY_EXPIRED;
/* If 'expire' action is paused, for whatever reason, then don't expire any key.
* Typically, at the end of the pause we will properly expire the key OR we
* will have failed over and the new primary will send us the expire. */
if (isPausedActionsWithUpdate(PAUSE_ACTION_EXPIRE)) return KEY_EXPIRED;
/* The key needs to be converted from static to heap before deleted */
int static_key = key->refcount == OBJ_STATIC_REFCOUNT;
if (static_key) {
key = createStringObject(key->ptr, sdslen(key->ptr));
}
/* Delete the key */
deleteExpiredKeyAndPropagate(db,key);
if (static_key) {
decrRefCount(key);
}
return KEY_DELETED;
}
One important point in this logic is that expiration on replicas is controlled by the master. During command replication from the master, keys are not treated as expired by the replica.
Periodic deletion
The periodic deletion logic is in expire.c, mainly inside void activeExpireCycle(int type):
void activeExpireCycle(int type) {
/* Adjust the running parameters according to the configured expire
* effort. The default effort is 1, and the maximum configurable effort
* is 10. */
unsigned long
effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
// 每次循环取出过期键的数量
config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
// FAST 模式下的执行周期
config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
// SLOW 模式的执行周期
config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
2*effort,
config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
effort;
...........
The periodic check frequency is configured in redis.conf. The default hz 10 means Redis performs expiration checks 10 times per second.
Memory eviction strategies
When Redis memory usage exceeds the configured maximum, Redis uses an eviction policy to remove eligible keys and keep the server running efficiently.
The maximum memory is configured with:
maxmemory <bytes>inredis.conf
If it is not set, Redis has no configured limit by default, though the practical maximum is constrained by physical memory.
Redis supports eight eviction policies:
noeviction: do not delete data; return an error when memory is insufficient. This is the default.volatile-lru: evict the least recently used keys among keys with expiration.volatile-lfu: evict the least frequently used keys among keys with expiration.volatile-ttl: evict keys that will expire soon.volatile-random: randomly evict keys with expiration.allkeys-lru: evict least recently used keys among all keys.allkeys-lfu: evict least frequently used keys among all keys.allkeys-random: randomly evict among all keys.
LRU algorithm
LRU stands for Least Recently Used. It evicts data that has not been used recently.
A traditional LRU implementation usually uses a linked list:
- Elements are ordered by access time.
- Recently accessed keys move to the head.
- When eviction is needed, the tail element is removed.
Redis does not use a traditional linked-list LRU implementation. For massive datasets, maintaining a full linked list introduces extra memory overhead and hurts cache performance. Redis therefore uses an approximate LRU algorithm.
The redisObject structure in server.h contains an lru field:
struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
};
The lru field is initialized when the object is created:
// typedef struct redisObject robj;
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
o->type = type;
o->encoding = OBJ_ENCODING_RAW;
o->ptr = ptr;
o->refcount = 1;
o->lru = 0;
return o;
}
void initObjectLRUOrLFU(robj *o) {
if (o->refcount == OBJ_SHARED_REFCOUNT)
return;
/* Set the LRU to the current lruclock (minutes resolution), or
* alternatively the LFU counter. */
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes() << 8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
return;
}
Redis stores an LRU timestamp in each object, based on a global LRU clock. When a key is accessed, Redis updates this field inside lookupKey in db.c:
robj *lookupKey(redisDb *db, robj *key, int flags) {
// 通过 dbFind 函数查找给定的键(key)如果找到,则获取键对应的值
dictEntry *de = dbFind(db, key->ptr);
robj *val = NULL;
if (de) {
val = dictGetVal(de);
/* Forcing deletion of expired keys on a replica makes the replica
* inconsistent with the master. We forbid it on readonly replicas, but
* we have to allow it on writable replicas to make write commands
* behave consistently.
*
* It's possible that the WRITE flag is set even during a readonly
* command, since the command may trigger events that cause modules to
* perform additional writes. */
// 处理键过期的情况
int is_ro_replica = server.masterhost && server.repl_slave_ro;
int expire_flags = 0;
if (flags & LOOKUP_WRITE && !is_ro_replica)
expire_flags |= EXPIRE_FORCE_DELETE_EXPIRED;
if (flags & LOOKUP_NOEXPIRE)
expire_flags |= EXPIRE_AVOID_DELETE_EXPIRED;
if (expireIfNeeded(db, key, expire_flags) != KEY_VALID) {
/* The key is no longer valid. */
val = NULL;
}
}
if (val) {
/* Update the access time for the ageing algorithm.
* Don't do it if we have a saving child, as this will trigger
* a copy on write madness. */
// 更新访问时间
if (server.current_client && server.current_client->flags & CLIENT_NO_TOUCH &&
server.current_client->cmd->proc != touchCommand)
flags |= LOOKUP_NOTOUCH;
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val); // 策略为 LFU,更新使用频率
} else {
val->lru = LRU_CLOCK(); // 策略为 LRU,更新时间戳
}
}
if (!(flags & (LOOKUP_NOSTATS | LOOKUP_WRITE)))
server.stat_keyspace_hits++;
/* TODO: Use separate hits stats for WRITE */
} else {
if (!(flags & (LOOKUP_NONOTIFY | LOOKUP_WRITE)))
notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
if (!(flags & (LOOKUP_NOSTATS | LOOKUP_WRITE)))
server.stat_keyspace_misses++;
/* TODO: Use separate misses stats and notify event for WRITE */
}
return val;
}
When Redis needs to evict memory, it uses random sampling. In evict.c, Redis defines an eviction pool:
struct evictionPoolEntry {
unsigned long long idle; /* Object idle time (inverse frequency for LFU) */
sds key; /* Key name. */
sds cached; /* Cached SDS object for key name. */
int dbid; /* Key DB number. */
int slot; /* Slot. */
};
Keys to be evicted are filled into the pool through evictionPoolPopulate:
int evictionPoolPopulate(redisDb *db, kvstore *samplekvs, struct evictionPoolEntry *pool) {
int j, k, count;
dictEntry *samples[server.maxmemory_samples];
int slot = kvstoreGetFairRandomDictIndex(samplekvs);
// 从字典中获取一些键,结果存放到 samples 中,并且返回获取的键的数量。所选取的键的数量不能超过 server.maxmemory_samples
count = kvstoreDictGetSomeKeys(samplekvs,slot,samples,server.maxmemory_samples);
// 循环采样,对抽样得到的键进行处理
for (j = 0; j < count; j++) {
unsigned long long idle;
sds key;
robj *o;
dictEntry *de;
de = samples[j];
key = dictGetKey(de);
/* If the dictionary we are sampling from is not the main
* dictionary (but the expires one) we need to lookup the key
* again in the key dictionary to obtain the value object. */
if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {
if (samplekvs != db->keys)
de = kvstoreDictFind(db->keys, slot, key);
o = dictGetVal(de);
}
............
LFU algorithm
LFU stands for Least Frequently Used. It evicts data based on access frequency. The core assumption is: if data has been accessed often in the past, it is more likely to be accessed frequently in the future.
A traditional LFU implementation also often uses linked-list structures:
- Elements are ordered by access count from high to low.
- Newly inserted elements are placed near the tail.
- Access increases the count.
- During eviction, elements are sorted by frequency, and ties are resolved by time; the least frequently used tail element is removed.
Redis again uses an approximate LFU implementation.
The same redisObject structure is used:
struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
};
Under LRU, the lru field stores a timestamp. Under LFU, the same field is split into two parts:
- Lower 8 bits: counter, initialized with
LFU_INIT_VAL = 5. - Higher 16 bits: Unix timestamp with minute-level precision.
lookupKey updates LFU data by calling updateLFU:
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val); // 策略为 LFU,更新使用频率
} else {
val->lru = LRU_CLOCK(); // 策略为 LRU,更新时间戳
}
}
The update logic is:
void updateLFU(robj *val) {
// 根据距离上次访问的时长,衰减访问次数
unsigned long counter = LFUDecrAndReturn(val);
// 根据当前访问更新访问次数
counter = LFULogIncr(counter);
// 更新 lru 变量值
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
LFU eviction is similar to LRU eviction: candidate keys are placed into the eviction pool by evictionPoolPopulate. The difference is how the score is calculated:
/* Calculate the idle time according to the policy. This is called
* idle just because the code initially handled LRU, but is in fact
* just a score where a higher score means better candidate. */
if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
idle = estimateObjectIdleTime(o);
} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
/* When we use an LRU policy, we sort the keys by idle time
* so that we expire keys starting from greater idle time.
* However when the policy is an LFU one, we have a frequency
* estimation, and we want to evict keys with lower frequency
* first. So inside the pool we put objects using the inverted
* frequency subtracting the actual frequency to the maximum
* frequency of 255. */
idle = 255-LFUDecrAndReturn(o);
} else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
/* In this case the sooner the expire the better. */
idle = ULLONG_MAX - (long)dictGetVal(de);
} else {
serverPanic("Unknown eviction policy in evictionPoolPopulate()");
}
For LFU, Redis uses an inverted frequency score: lower actual frequency means a higher eviction score and therefore a better eviction candidate.
Redis high availability
Master-replica replication
Redis replication copies data from one Redis server to other Redis servers. The source node is the master, and the receiving nodes are replicas.
The data flow is one-way: data can only flow from master to replica.
Redis replication is asynchronous in two senses:
- When the master synchronizes data to replicas, it does so asynchronously, so the master can continue handling other requests.
- Replicas also receive synchronization data asynchronously.

A common topology is one master with multiple replicas:
- The master handles data modification.
- Replicas handle read requests. In practice, one master and one replica is also common.
If there are too many replicas, synchronization can put pressure on the master, because the master may need to generate and send RDB files repeatedly. This can affect master performance.
One way to reduce that pressure is to let replicas serve as upstream nodes for other replicas.

Full synchronization
Full synchronization usually happens during the first synchronization between master and replica.

The process is:
- The replica sends a synchronization request to the master.
- The master returns
replid, and the replica updates it. - The master runs
bgsave, generates an RDB file, and sends it to the replica. - The replica deletes its local data, receives the RDB file, and loads it.
- If the master receives new commands during synchronization, it writes them into a replica buffer.
- The replica applies the buffered commands locally and records the latest data offset.
Incremental synchronization
If the master and replica disconnect, the replica attempts to obtain only the data it missed after reconnecting. This is partial synchronization, also called partial replication.

The mechanism is:
- The master records a pseudo-random
replicationIdto identify the current dataset version, and it records the current dataset offset. - The master continuously writes received commands into
repl_backlogand updates the offset. - During incremental synchronization, the master finds the commands after the replica's offset in
repl_backlogand sends them back. - The replica receives the data, writes it locally, and updates its offset to match the master.
Notes:
repl_backloghas a size limit. Once full, new data overwrites old data.- If a replica is disconnected for too long and the required backlog data has been overwritten, incremental synchronization can no longer be performed based on the log.
Sentinel
With only master-replica replication, if the master goes down, replicas cannot automatically take over as the master. The whole system may stop handling business writes normally.
Sentinel solves this by monitoring Redis instances and promoting a replica to master when the old master fails.
Service monitoring

Sentinel monitors service status with heartbeat checks:
- Every 1 second, Sentinel sends
PINGto each instance in the cluster. - Subjective down: one Sentinel does not receive a response from an instance and considers it down.
- Objective down: when enough Sentinel nodes agree that the instance is subjectively down, the instance is considered objectively down.
Election rules
When Sentinel needs to select a new master from replicas, the priority is:
- Prefer the replica that has been disconnected from the master for the shortest time.
- Then compare
slave-priority; the smaller value has higher priority. - If
slave-priorityis the same, compare offset; the larger offset has higher priority. - Finally compare replica run ID; the smaller ID has higher priority.
Split-brain problem
A split-brain scenario can occur when Sentinel and Redis instances are in different network partitions. If network instability prevents Sentinel from receiving heartbeats from the old master, Sentinel may promote a replica as the new master.
At the same time:
- Clients may still be writing to the old master.
- After the network recovers, the old master is forced to become a replica, and the data written to it during the split may be lost.
A common mitigation is to configure Redis parameters:
min-replicas-to-write 1: require at least one replica before accepting writes.min-replicas-max-lag 5: replication and synchronization lag must not exceed 5 seconds.
If split-brain occurs, the original master rejects client write requests when these conditions are not met, reducing the chance of large-scale data loss.