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

Redis与MySQL结合实现灵活的数据查询方案

2024-12-138.0k 阅读

数据库基础:Redis 与 MySQL 概述

Redis 简介

Redis 是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set),这使得它在处理不同类型的数据和业务场景时非常灵活。

Redis 之所以性能卓越,主要得益于其基于内存的存储方式,数据的读写操作直接在内存中进行,避免了磁盘 I/O 的开销,这使得它能够达到极高的读写速度。例如,在简单的字符串操作场景下,Redis 可以轻松达到每秒数万甚至数十万次的读写操作。

MySQL 简介

MySQL 是一种广泛使用的关系型数据库管理系统,它基于 SQL(Structured Query Language)语言进行数据的存储、查询和管理。MySQL 将数据存储在表结构中,通过行和列的方式组织数据,每个表都有特定的结构和约束。

MySQL 的优势在于其强大的数据持久化能力和事务处理能力。它适合存储大量结构化数据,并能保证数据的完整性和一致性。例如,在电商系统的订单管理模块中,MySQL 可以可靠地存储订单的详细信息,包括商品信息、用户信息、支付信息等,并且能够通过事务机制确保订单处理过程中的数据一致性,如库存的扣减和订单状态的更新等操作要么全部成功,要么全部失败。

为何结合 Redis 与 MySQL

优势互补

  1. 性能方面:Redis 的高性能读写适合处理高频访问的数据,如热门商品的信息、用户的登录状态等。而 MySQL 虽然在读写速度上相对较慢,但它擅长处理复杂的查询和大量数据的持久化存储。例如,在一个新闻网站中,热门文章的标题、摘要等信息可以存储在 Redis 中,以快速响应用户的浏览请求;而文章的完整内容、评论等大量数据则存储在 MySQL 中,需要时再从 MySQL 中读取并通过 Redis 的缓存加速下次访问。
  2. 数据结构方面:Redis 的丰富数据结构可以满足多样化的数据处理需求。例如,使用 Redis 的哈希结构可以方便地存储用户的多种属性信息,而 MySQL 的表结构适合存储关系型数据,如用户与订单之间的关联关系等。通过结合两者,可以充分利用它们在数据结构上的优势,实现更灵活的数据管理。

应对不同业务场景

  1. 缓存热点数据:在高并发的 Web 应用中,许多数据是频繁被访问的,如首页展示的热门商品列表、网站的配置信息等。将这些热点数据存储在 Redis 中,可以大大减轻 MySQL 的负载,提高系统的响应速度。当数据发生变化时,及时更新 Redis 缓存和 MySQL 数据库,保证数据的一致性。
  2. 复杂查询与数据分析:对于涉及多表关联、统计分析等复杂查询场景,MySQL 凭借其强大的 SQL 功能可以轻松应对。例如,在电商系统中统计某个时间段内不同地区的销售总额,MySQL 可以通过 JOIN 操作和聚合函数实现。而 Redis 在这种复杂查询场景下相对较弱,但它可以作为结果的缓存,下次相同查询直接从 Redis 中获取结果,提高查询效率。

结合 Redis 与 MySQL 实现数据查询方案

基本思路

  1. 读操作流程:当应用程序发起数据查询请求时,首先尝试从 Redis 中获取数据。如果 Redis 中存在所需数据,则直接返回给应用程序,这样可以快速响应请求,提高系统性能。如果 Redis 中没有命中数据,则从 MySQL 中查询数据。查询到数据后,将数据同时写入 Redis 缓存,以便下次查询可以直接从 Redis 中获取,减少对 MySQL 的压力。
  2. 写操作流程:当应用程序进行数据写入操作时,首先更新 MySQL 数据库,确保数据的持久化存储。然后,为了保证数据一致性,需要更新 Redis 缓存中的相关数据。如果 Redis 缓存更新失败,需要有相应的重试机制或者记录日志以便后续处理。

具体实现

  1. 使用 Python 示例:假设我们有一个简单的用户信息管理系统,用户信息存储在 MySQL 中,同时使用 Redis 缓存经常访问的用户信息。以下是使用 Python 和相关库(pymysql 操作 MySQL,redis - py 操作 Redis)实现上述流程的代码示例。
import pymysql
import redis


# 连接 MySQL 数据库
def get_mysql_connection():
    return pymysql.connect(
        host='localhost',
        user='root',
        password='password',
        database='test',
        charset='utf8mb4'
    )


# 连接 Redis
def get_redis_connection():
    return redis.Redis(host='localhost', port=6379, db = 0)


# 从 Redis 获取用户信息
def get_user_from_redis(redis_conn, user_id):
    return redis_conn.get(f'user:{user_id}')


# 从 MySQL 获取用户信息
def get_user_from_mysql(mysql_conn, user_id):
    with mysql_conn.cursor() as cursor:
        sql = "SELECT * FROM users WHERE id = %s"
        cursor.execute(sql, (user_id,))
        result = cursor.fetchone()
        return result


# 将用户信息写入 Redis
def set_user_to_redis(redis_conn, user_id, user_info):
    redis_conn.set(f'user:{user_id}', str(user_info))


# 获取用户信息主函数
def get_user(user_id):
    redis_conn = get_redis_connection()
    mysql_conn = get_mysql_connection()
    user_info = get_user_from_redis(redis_conn, user_id)
    if user_info:
        print('从 Redis 获取到用户信息')
        return user_info
    else:
        user_info = get_user_from_mysql(mysql_conn, user_id)
        if user_info:
            print('从 MySQL 获取到用户信息,并写入 Redis')
            set_user_to_redis(redis_conn, user_id, user_info)
        else:
            print('用户不存在')
        mysql_conn.close()
        return user_info


# 示例调用
if __name__ == '__main__':
    user_id = 1
    get_user(user_id)
  1. 使用 Java 示例:同样以用户信息管理系统为例,以下是使用 Java 和相关库(JDBC 操作 MySQL,Jedis 操作 Redis)实现的代码示例。
import redis.clients.jedis.Jedis;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;


public class UserService {
    private static final String URL = "jdbc:mysql://localhost:3306/test";
    private static final String USER = "root";
    private static final String PASSWORD = "password";
    private static final Jedis jedis = new Jedis("localhost", 6379);


    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASSWORD);
    }


    public static String getUserFromRedis(String userId) {
        return jedis.get("user:" + userId);
    }


    public static String getUserFromMySQL(String userId) {
        try (Connection conn = getConnection()) {
            String sql = "SELECT * FROM users WHERE id =?";
            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                pstmt.setString(1, userId);
                try (ResultSet rs = pstmt.executeQuery()) {
                    if (rs.next()) {
                        return rs.getString("name") + "," + rs.getString("age");
                    }
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }


    public static void setUserToRedis(String userId, String userInfo) {
        jedis.set("user:" + userId, userInfo);
    }


    public static String getUser(String userId) {
        String userInfo = getUserFromRedis(userId);
        if (userInfo!= null) {
            System.out.println("从 Redis 获取到用户信息");
            return userInfo;
        } else {
            userInfo = getUserFromMySQL(userId);
            if (userInfo!= null) {
                System.out.println("从 MySQL 获取到用户信息,并写入 Redis");
                setUserToRedis(userId, userInfo);
            } else {
                System.out.println("用户不存在");
            }
            return userInfo;
        }
    }


    public static void main(String[] args) {
        String userId = "1";
        System.out.println(getUser(userId));
    }
}

数据一致性问题及解决方案

数据不一致的原因

  1. 缓存更新不及时:在写操作时,如果先更新了 MySQL 数据库,而在更新 Redis 缓存时出现网络故障、系统崩溃等问题,就会导致 Redis 中的数据与 MySQL 不一致。例如,在电商系统中,如果商品价格在 MySQL 中更新成功,但 Redis 缓存中的价格未更新,用户在访问商品详情时可能看到的还是旧价格。
  2. 读写并发问题:在高并发场景下,可能会出现读操作先从 Redis 中未命中数据,然后从 MySQL 中读取数据的过程中,写操作更新了 MySQL 数据但还未更新 Redis 缓存,此时读操作将 MySQL 中的旧数据写入 Redis 缓存,导致 Redis 缓存中的数据为旧数据,与 MySQL 不一致。

解决方案

  1. 重试机制:在更新 Redis 缓存失败时,应用程序可以设置重试次数,例如重试 3 次。如果多次重试仍失败,则记录详细的日志信息,包括操作类型、失败时间、失败原因等,以便后续人工排查。例如,在上述 Python 代码的写操作流程中,可以添加如下重试逻辑:
def set_user_to_redis(redis_conn, user_id, user_info):
    max_retries = 3
    retries = 0
    while retries < max_retries:
        try:
            redis_conn.set(f'user:{user_id}', str(user_info))
            return True
        except redis.RedisError as e:
            retries += 1
            print(f'更新 Redis 缓存失败,重试 {retries} 次,原因:{e}')
    print('多次重试更新 Redis 缓存失败,请检查')
    return False
  1. 缓存失效策略:设置合理的缓存过期时间,即使出现数据不一致的情况,在缓存过期后,下次查询会重新从 MySQL 中获取数据并更新缓存,保证数据的最终一致性。例如,对于一些不太敏感的数据,如商品的浏览量统计,可以设置较短的缓存过期时间,如 1 小时。在上述代码示例中,可以在 set_user_to_redis 函数中添加过期时间设置:
def set_user_to_redis(redis_conn, user_id, user_info):
    max_retries = 3
    retries = 0
    while retries < max_retries:
        try:
            redis_conn.setex(f'user:{user_id}', 3600, str(user_info))  # 设置过期时间为 1 小时
            return True
        except redis.RedisError as e:
            retries += 1
            print(f'更新 Redis 缓存失败,重试 {retries} 次,原因:{e}')
    print('多次重试更新 Redis 缓存失败,请检查')
    return False
  1. 读写锁:在读写并发场景下,可以使用读写锁机制。写操作时获取写锁,禁止其他读写操作,保证写操作完成后再进行读操作。读操作时获取读锁,可以允许多个读操作同时进行,但禁止写操作。例如,在 Java 中可以使用 ReentrantReadWriteLock 来实现读写锁机制:
import java.util.concurrent.locks.ReentrantReadWriteLock;


public class UserServiceWithLock {
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();


    // 读操作
    public static String getUser(String userId) {
        readLock.lock();
        try {
            String userInfo = getUserFromRedis(userId);
            if (userInfo!= null) {
                System.out.println("从 Redis 获取到用户信息");
                return userInfo;
            } else {
                writeLock.lock();
                try {
                    userInfo = getUserFromMySQL(userId);
                    if (userInfo!= null) {
                        System.out.println("从 MySQL 获取到用户信息,并写入 Redis");
                        setUserToRedis(userId, userInfo);
                    } else {
                        System.out.println("用户不存在");
                    }
                    return userInfo;
                } finally {
                    writeLock.unlock();
                }
            }
        } finally {
            readLock.unlock();
        }
    }


    // 写操作
    public static void updateUser(String userId, String newUserInfo) {
        writeLock.lock();
        try {
            // 更新 MySQL 数据库
            updateUserInMySQL(userId, newUserInfo);
            // 更新 Redis 缓存
            setUserToRedis(userId, newUserInfo);
        } finally {
            writeLock.unlock();
        }
    }


    // 其他数据库操作和 Redis 操作方法同之前示例
}

优化策略

缓存预热

  1. 含义:缓存预热是指在系统上线或者重启后,提前将一些热点数据加载到 Redis 缓存中,避免上线初期由于大量缓存未命中导致 MySQL 压力过大。例如,在电商系统中,在系统启动时将热门商品的信息提前加载到 Redis 缓存中,这样用户访问热门商品时就可以直接从 Redis 中获取数据,提高系统的响应速度。
  2. 实现方式:可以通过定时任务或者启动脚本实现缓存预热。在 Python 中,可以使用 APScheduler 库实现定时任务来进行缓存预热:
from apscheduler.schedulers.background import BackgroundScheduler
import pymysql
import redis


# 连接 MySQL 数据库
def get_mysql_connection():
    return pymysql.connect(
        host='localhost',
        user='root',
        password='password',
        database='test',
        charset='utf8mb4'
    )


# 连接 Redis
def get_redis_connection():
    return redis.Redis(host='localhost', port=6379, db = 0)


# 从 MySQL 获取热门商品信息
def get_hot_products_from_mysql(mysql_conn):
    with mysql_conn.cursor() as cursor:
        sql = "SELECT * FROM products WHERE is_hot = 1"
        cursor.execute(sql)
        results = cursor.fetchall()
        return results


# 将热门商品信息写入 Redis
def set_hot_products_to_redis(redis_conn, products):
    for product in products:
        product_id = product[0]
        product_info = str(product)
        redis_conn.set(f'product:{product_id}', product_info)


# 缓存预热函数
def warm_up_cache():
    mysql_conn = get_mysql_connection()
    redis_conn = get_redis_connection()
    hot_products = get_hot_products_from_mysql(mysql_conn)
    set_hot_products_to_redis(redis_conn, hot_products)
    mysql_conn.close()


if __name__ == '__main__':
    scheduler = BackgroundScheduler()
    scheduler.add_job(warm_up_cache, 'interval', minutes = 1440)  # 每天执行一次缓存预热
    scheduler.start()
    print('缓存预热任务已启动')
    try:
        while True:
            pass
    except KeyboardInterrupt:
        scheduler.shutdown()
        print('缓存预热任务已停止')

缓存淘汰策略优化

  1. Redis 缓存淘汰策略:Redis 提供了多种缓存淘汰策略,如 no - eviction(不淘汰任何数据,当内存不足时,新写入操作会报错)、allkeys - lru(在所有键中,使用 LRU 算法淘汰最近最少使用的键)、volatile - lru(在设置了过期时间的键中,使用 LRU 算法淘汰最近最少使用的键)等。根据业务场景合理选择缓存淘汰策略可以提高缓存的利用率。
  2. 动态调整淘汰策略:可以根据系统的运行状态动态调整缓存淘汰策略。例如,在系统访问高峰期,可以将淘汰策略调整为 allkeys - lru,优先淘汰不常用的数据,保证热点数据的缓存命中率;在系统访问低谷期,可以调整为 no - eviction,避免不必要的数据淘汰,减少从 MySQL 中读取数据的开销。在 Python 中,可以通过 Redis 客户端库动态设置淘汰策略:
import redis


redis_conn = redis.Redis(host='localhost', port=6379, db = 0)


# 设置缓存淘汰策略为 allkeys - lru
redis_conn.config_set('maxmemory - policy', 'allkeys - lru')

批量查询优化

  1. 减少数据库查询次数:在需要查询多个数据项时,如果逐个查询会增加 MySQL 的负载和网络开销。可以使用 MySQL 的 IN 语句或者 Redis 的批量操作命令来减少查询次数。例如,在查询多个用户信息时,可以使用 IN 语句一次性从 MySQL 中获取多个用户的数据:
import pymysql
import redis


# 连接 MySQL 数据库
def get_mysql_connection():
    return pymysql.connect(
        host='localhost',
        user='root',
        password='password',
        database='test',
        charset='utf8mb4'
    )


# 连接 Redis
def get_redis_connection():
    return redis.Redis(host='localhost', port=6379, db = 0)


# 从 Redis 批量获取用户信息
def get_users_from_redis(redis_conn, user_ids):
    return redis_conn.mget([f'user:{user_id}' for user_id in user_ids])


# 从 MySQL 批量获取用户信息
def get_users_from_mysql(mysql_conn, user_ids):
    with mysql_conn.cursor() as cursor:
        placeholders = ', '.join(['%s'] * len(user_ids))
        sql = f"SELECT * FROM users WHERE id IN ({placeholders})"
        cursor.execute(sql, user_ids)
        results = cursor.fetchall()
        return results


# 将用户信息批量写入 Redis
def set_users_to_redis(redis_conn, user_infos):
    pipe = redis_conn.pipeline()
    for user_info in user_infos:
        user_id = user_info[0]
        pipe.set(f'user:{user_id}', str(user_info))
    pipe.execute()


# 批量获取用户信息主函数
def get_users(user_ids):
    redis_conn = get_redis_connection()
    mysql_conn = get_mysql_connection()
    user_infos = get_users_from_redis(redis_conn, user_ids)
    missing_user_ids = []
    for i, user_info in enumerate(user_infos):
        if user_info is None:
            missing_user_ids.append(user_ids[i])
    if missing_user_ids:
        mysql_user_infos = get_users_from_mysql(mysql_conn, missing_user_ids)
        set_users_to_redis(redis_conn, mysql_user_infos)
        user_infos = get_users_from_redis(redis_conn, user_ids)
    mysql_conn.close()
    return user_infos


# 示例调用
if __name__ == '__main__':
    user_ids = [1, 2, 3]
    print(get_users(user_ids))

应用场景举例

电商系统

  1. 商品详情页:商品的基本信息,如名称、价格、图片等高频访问的数据存储在 Redis 中,以快速响应用户请求。商品的详细描述、规格参数等相对低频访问的数据存储在 MySQL 中。当用户访问商品详情页时,先从 Redis 中获取基本信息,如果 Redis 中没有命中,则从 MySQL 中读取并更新 Redis 缓存。
  2. 购物车功能:用户的购物车信息可以存储在 Redis 中,利用 Redis 的哈希结构可以方便地存储商品 ID、数量等信息。购物车的持久化可以通过定时将 Redis 中的购物车数据同步到 MySQL 中实现,例如每隔 5 分钟同步一次,保证用户数据的安全性和一致性。

新闻资讯系统

  1. 首页新闻列表:首页展示的热门新闻标题、摘要等信息存储在 Redis 中,以提高页面加载速度。新闻的详细内容存储在 MySQL 中。当用户点击新闻查看详细内容时,先从 Redis 中判断是否存在缓存,如果不存在则从 MySQL 中读取并更新 Redis 缓存。
  2. 新闻评论功能:新闻的评论数据量大且关系复杂,适合存储在 MySQL 中。为了提高评论加载速度,可以在 Redis 中缓存热门新闻的最新几条评论,用户查看评论时先从 Redis 中获取,如果 Redis 中没有或者需要查看更多评论,则从 MySQL 中查询。

社交平台

  1. 用户个人资料:用户的基本资料,如昵称、头像、简介等经常被访问的数据存储在 Redis 中。用户的详细资料,如教育背景、工作经历等存储在 MySQL 中。当用户访问自己或他人的个人资料页时,先从 Redis 中获取基本资料,如果 Redis 中没有命中,则从 MySQL 中读取并更新 Redis 缓存。
  2. 好友关系:用户的好友列表可以存储在 Redis 的集合结构中,方便进行添加、删除好友等操作。好友关系的持久化和复杂查询,如查询共同好友等,可以通过 MySQL 实现。例如,在添加好友时,先在 Redis 中更新好友列表,然后将好友关系同步到 MySQL 中,保证数据的一致性。