MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis缓存失效策略与数据一致性保障

2023-05-064.3k 阅读

Redis缓存失效策略

1. 定时删除

定时删除策略是在设置键的过期时间时,同时创建一个定时器,当过期时间到达时,由定时器来执行对键的删除操作。

优点

  • 能及时释放内存,保证内存的高效使用,因为一旦过期时间到,立即执行删除操作,不会让过期键长时间占用内存。

缺点

  • 对CPU不友好,因为要为每个设置过期时间的键创建定时器。在高并发场景下,大量键同时设置过期时间,会产生大量定时器,消耗大量CPU资源来处理这些定时器的调度和执行删除操作。

代码示例(Python + Redis-py)

import redis
import time

r = redis.Redis(host='localhost', port=6379, db = 0)
# 设置一个键值对,并设置过期时间为5秒
r.setex('key1', 5, 'value1')

# 模拟定时删除操作,这里只是简单演示思路,实际定时器逻辑更复杂
def check_expired():
    while True:
        keys = r.keys('*')
        for key in keys:
            ttl = r.ttl(key)
            if ttl == 0:
                r.delete(key)
        time.sleep(1)

import threading
t = threading.Thread(target=check_expired)
t.start()

2. 惰性删除

惰性删除策略是指键过期了也不会立即删除,而是当访问这个键时,检查键是否过期,如果过期则删除该键并返回空值(例如在Redis中返回nil)。

优点

  • 对CPU友好,只有在访问键时才会检查是否过期并执行删除操作,避免了定时删除带来的大量定时器开销。

缺点

  • 对内存不友好,过期键可能会长时间占用内存,直到被访问才会删除。如果有大量过期键一直未被访问,会导致内存浪费,甚至可能引发内存溢出问题。

代码示例(Python + Redis-py)

import redis

r = redis.Redis(host='localhost', port=6379, db = 0)
# 设置一个键值对,并设置过期时间为5秒
r.setex('key2', 5, 'value2')

# 模拟惰性删除,访问键时检查是否过期
def get_key(key):
    value = r.get(key)
    if value is None:
        # 这里可以添加日志记录等操作
        return None
    return value

# 假设在5秒后访问
import time
time.sleep(6)
print(get_key('key2'))

3. 定期删除

定期删除策略是每隔一段时间,Redis会随机从设置了过期时间的键中取出一部分键,检查这些键是否过期,如果过期则删除。

优点

  • 是一种折中的方案,既不会像定时删除那样消耗大量CPU资源,也不会像惰性删除那样导致过期键长时间占用内存。通过合理设置定期删除的时间间隔和每次检查的键数量,可以在CPU和内存之间找到一个较好的平衡点。

缺点

  • 如果设置的定期检查时间间隔过长,或者每次检查的键数量过少,可能会导致部分过期键长时间不能被删除,占用内存。反之,如果时间间隔过短,每次检查键数量过多,又会消耗较多CPU资源。

代码示例(假设在Redis内部实现定期删除逻辑的伪代码)

// 定期删除函数
void periodicDelete() {
    int keys_per_iteration = 100;
    int loops = 10;
    for (int i = 0; i < loops; i++) {
        // 从过期字典中随机获取一部分键
        dictEntry **de = dictGetSomeKeys(redisDb->expires, keys_per_iteration);
        if (de == NULL) return;
        for (int j = 0; j < keys_per_iteration; j++) {
            robj *key = dictGetKey(de[j]);
            if (dictFind(redisDb->dict, key) != NULL && isExpired(key)) {
                // 删除键
                dbDelete(redisDb, key);
            }
        }
        zfree(de);
    }
}

数据一致性保障

1. 读写顺序问题

在使用Redis缓存时,读写顺序不当容易导致数据不一致。常见的场景是先读缓存,缓存未命中后读数据库,然后将数据库数据写入缓存。如果在写入缓存之前,另一个线程修改了数据库数据,就会导致缓存数据和数据库数据不一致。

解决方案

  • 读写锁:在读取数据时加读锁,在写入数据时加写锁。读锁可以共享,多个读操作可以同时进行,但写锁必须独占,当有写操作进行时,其他读写操作都要等待。

代码示例(Java + Jedis + Redisson实现读写锁)

import org.redisson.Redisson;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import redis.clients.jedis.Jedis;

public class DataConsistencyExample {
    private static final RedissonClient redissonClient;
    private static final Jedis jedis;

    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redissonClient = Redisson.create(config);
        jedis = new Jedis("127.0.0.1", 6379);
    }

    public static String getData(String key) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock(key);
        rwLock.readLock().lock();
        try {
            String value = jedis.get(key);
            if (value == null) {
                // 从数据库读取数据,这里假设从数据库获取数据的方法为getFromDB
                value = getFromDB(key);
                if (value != null) {
                    jedis.set(key, value);
                }
            }
            return value;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public static void setData(String key, String value) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock(key);
        rwLock.writeLock().lock();
        try {
            // 写入数据库,这里假设写入数据库的方法为setToDB
            setToDB(key, value);
            jedis.set(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    private static String getFromDB(String key) {
        // 实际从数据库获取数据的逻辑
        return "data from db";
    }

    private static void setToDB(String key, String value) {
        // 实际写入数据库的逻辑
    }
}

2. 缓存更新策略

  • Cache-Aside模式:应用程序先更新数据库,再删除缓存。这种模式下,读操作先从缓存读取,缓存未命中则从数据库读取并写入缓存;写操作先更新数据库,然后删除缓存。

优点

  • 实现简单,在大多数场景下能有效保证数据一致性。

缺点

  • 如果在更新数据库成功后,删除缓存失败,会导致数据不一致。

代码示例(Python + Redis-py + MySQLdb)

import redis
import MySQLdb

r = redis.Redis(host='localhost', port=6379, db = 0)
db = MySQLdb.connect(host="localhost", user="root", passwd="password", db="test")
cursor = db.cursor()

def update_data(key, value):
    try:
        # 更新数据库
        sql = "UPDATE your_table SET data = %s WHERE key = %s"
        cursor.execute(sql, (value, key))
        db.commit()
        # 删除缓存
        r.delete(key)
    except Exception as e:
        print(f"Error: {e}")
        db.rollback()
    finally:
        cursor.close()
        db.close()

def get_data(key):
    value = r.get(key)
    if value is None:
        sql = "SELECT data FROM your_table WHERE key = %s"
        cursor.execute(sql, (key,))
        result = cursor.fetchone()
        if result:
            value = result[0]
            r.set(key, value)
        cursor.close()
        db.close()
    return value
  • Write-Through模式:应用程序更新数据库的同时更新缓存。

优点

  • 数据一致性强,因为数据库和缓存同时更新,不会出现数据库更新成功而缓存未更新的情况。

缺点

  • 性能相对较低,因为每次写操作都要同时更新数据库和缓存,增加了写操作的时间开销。

代码示例(Java + Jedis + JDBC实现Write - Through模式)

import redis.clients.jedis.Jedis;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class WriteThroughExample {
    private static final Jedis jedis;
    private static final String DB_URL = "jdbc:mysql://localhost:3306/test";
    private static final String DB_USER = "root";
    private static final String DB_PASSWORD = "password";

    static {
        jedis = new Jedis("127.0.0.1", 6379);
    }

    public static void updateData(String key, String value) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
            String sql = "UPDATE your_table SET data = ? WHERE key = ?";
            statement = connection.prepareStatement(sql);
            statement.setString(1, value);
            statement.setString(2, key);
            statement.executeUpdate();
            jedis.set(key, value);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static String getData(String key) {
        String value = jedis.get(key);
        if (value == null) {
            Connection connection = null;
            PreparedStatement statement = null;
            try {
                connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
                String sql = "SELECT data FROM your_table WHERE key = ?";
                statement = connection.prepareStatement(sql);
                statement.setString(1, key);
                java.sql.ResultSet resultSet = statement.executeQuery();
                if (resultSet.next()) {
                    value = resultSet.getString("data");
                    jedis.set(key, value);
                }
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                if (statement != null) {
                    try {
                        statement.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if (connection != null) {
                    try {
                        connection.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return value;
    }
}
  • Write-Behind模式:应用程序只更新缓存,缓存系统异步将数据更新到数据库。

优点

  • 性能高,因为写操作只需要更新缓存,不需要等待数据库更新完成,适合高并发写场景。

缺点

  • 数据一致性相对较弱,如果缓存系统出现故障,可能会导致部分数据丢失,无法同步到数据库。

代码示例(以Python的异步任务队列Celery为例模拟Write - Behind模式)

from celery import Celery
import redis
import MySQLdb

app = Celery('write_behind', broker='redis://localhost:6379/0')
r = redis.Redis(host='localhost', port=6379, db = 0)

@app.task
def update_db(key, value):
    db = MySQLdb.connect(host="localhost", user="root", passwd="password", db="test")
    cursor = db.cursor()
    try:
        sql = "UPDATE your_table SET data = %s WHERE key = %s"
        cursor.execute(sql, (value, key))
        db.commit()
    except Exception as e:
        print(f"Error: {e}")
        db.rollback()
    finally:
        cursor.close()
        db.close()

def write_data(key, value):
    r.set(key, value)
    update_db.delay(key, value)

def read_data(key):
    value = r.get(key)
    if value is None:
        db = MySQLdb.connect(host="localhost", user="root", passwd="password", db="test")
        cursor = db.cursor()
        try:
            sql = "SELECT data FROM your_table WHERE key = %s"
            cursor.execute(sql, (key,))
            result = cursor.fetchone()
            if result:
                value = result[0]
                r.set(key, value)
        except Exception as e:
            print(f"Error: {e}")
        finally:
            cursor.close()
            db.close()
    return value

3. 分布式环境下的数据一致性

在分布式系统中,多个节点可能同时操作缓存和数据库,数据一致性问题更加复杂。

  • 分布式锁:通过分布式锁保证同一时间只有一个节点能对数据进行写操作。常见的实现方式有基于Redis的分布式锁。

代码示例(Java + Redisson实现分布式锁)

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class DistributedLockExample {
    private static final RedissonClient redissonClient;

    static {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redissonClient = Redisson.create(config);
    }

    public static void writeData(String key, String value) {
        RLock lock = redissonClient.getLock(key);
        try {
            lock.lock();
            // 这里执行写数据库和更新缓存操作
            System.out.println("Writing data: " + value + " for key: " + key);
        } finally {
            lock.unlock();
        }
    }
}
  • 多副本数据同步:在分布式缓存中,通过数据同步机制保证多个副本之间的数据一致性。例如Redis Cluster通过节点之间的gossip协议进行数据同步。

结合使用多种策略保障数据一致性和缓存性能

在实际应用中,通常会结合多种缓存失效策略和数据一致性保障策略。例如,使用定期删除和惰性删除结合的方式来管理缓存过期,在数据一致性方面采用Cache-Aside模式,并结合分布式锁来处理分布式环境下的并发写操作。

以一个电商系统为例,商品信息缓存可以采用定期删除(如每隔1分钟检查一次部分商品缓存是否过期)和惰性删除结合的策略。在商品数据更新时,采用Cache-Aside模式,先更新数据库,再删除缓存。当多个节点同时更新商品数据时,使用分布式锁来保证同一时间只有一个节点能进行更新操作。

代码示例(综合示例,以Python为例,简化实现)

import redis
import MySQLdb
from celery import Celery
from redlock import Redlock

app = Celery('ecommerce_cache', broker='redis://localhost:6379/0')
r = redis.Redis(host='localhost', port=6379, db = 0)
db = MySQLdb.connect(host="localhost", user="root", passwd="password", db="ecommerce")
cursor = db.cursor()

# 定期删除任务
@app.task
def periodic_delete():
    keys = r.keys('product:*')
    for key in keys:
        ttl = r.ttl(key)
        if ttl == 0:
            r.delete(key)

# 获取商品信息
def get_product_info(product_id):
    key = f'product:{product_id}'
    value = r.get(key)
    if value is None:
        sql = "SELECT info FROM products WHERE id = %s"
        cursor.execute(sql, (product_id,))
        result = cursor.fetchone()
        if result:
            value = result[0]
            r.set(key, value)
    return value

# 更新商品信息
def update_product_info(product_id, new_info):
    # 使用Redlock实现分布式锁
    redlock = Redlock([{"host": "localhost", "port": 6379, "db": 0}], retry_count = 3)
    lock = redlock.lock(f'product_lock:{product_id}', 10000)
    if lock:
        try:
            sql = "UPDATE products SET info = %s WHERE id = %s"
            cursor.execute(sql, (new_info, product_id))
            db.commit()
            key = f'product:{product_id}'
            r.delete(key)
        except Exception as e:
            print(f"Error: {e}")
            db.rollback()
        finally:
            redlock.unlock(lock)
    else:
        print("Failed to acquire lock")

# 启动定期删除任务
import threading
t = threading.Thread(target=periodic_delete)
t.start()

通过合理组合这些策略,可以在保障数据一致性的同时,充分发挥Redis缓存的高性能优势,满足不同业务场景的需求。在实际开发中,还需要根据具体的业务特点、数据量、并发量等因素,对策略进行进一步的优化和调整。