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

Redis与MySQL读写分离缓存模式实战优化

2023-06-184.5k 阅读

Redis与MySQL读写分离缓存模式基础概念

1. 读写分离概念

在数据库应用场景中,读写分离是一种常用的架构模式。由于读操作(例如查询数据)和写操作(例如插入、更新、删除数据)在性能需求和资源占用上存在差异,将读操作和写操作分配到不同的数据库服务器上执行,可以提高系统的整体性能和可扩展性。

一般来说,读操作往往具有较高的并发量,而写操作相对较少但对数据一致性要求严格。通过读写分离,我们可以将读请求发送到专门的读服务器,写请求发送到主服务器。这样,主服务器专注于处理写操作,保证数据的一致性,而读服务器可以通过复制主服务器的数据来处理大量的读请求,减轻主服务器的负载。

2. Redis缓存

Redis是一种高性能的键值对存储数据库,因其出色的读写速度和丰富的数据结构,在缓存领域被广泛应用。在读写分离架构中,Redis常被用作缓存层,放置在应用程序和数据库之间。

当应用程序发起读请求时,首先会查询Redis缓存。如果缓存中存在所需数据,则直接从Redis返回,避免了对数据库的查询,大大提高了响应速度。如果缓存中没有数据,则查询数据库,将查询结果存入Redis缓存,以便后续相同请求可以直接从缓存获取。

3. 结合模式概述

将Redis缓存与MySQL读写分离相结合,可以构建一个高效的、可扩展的数据访问架构。应用程序在读操作时优先访问Redis缓存,命中则直接返回数据;未命中时从MySQL读服务器获取数据并更新Redis缓存。写操作则直接作用于MySQL主服务器,同时更新Redis缓存以保证数据一致性。

这种模式不仅利用了Redis的高速缓存能力,减少数据库读压力,还通过MySQL的读写分离,提升了系统整体的读写性能和稳定性。

实战前的准备工作

1. 环境搭建

  • 安装MySQL:可以从MySQL官方网站下载适合你操作系统的安装包进行安装。安装过程中,注意设置好root密码、端口号等基本配置。安装完成后,创建一个示例数据库和表,例如:
CREATE DATABASE test_db;
USE test_db;
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255),
    age INT
);
  • 安装Redis:Redis的安装也较为简单,在Linux系统上,可以通过包管理器(如apt - get或yum)进行安装,命令如下:
# 在Ubuntu上安装
sudo apt - get update
sudo apt - get install redis - server
# 在CentOS上安装
sudo yum install epel - release
sudo yum install redis

安装完成后,通过修改Redis配置文件(通常位于/etc/redis/redis.conf),可以对Redis进行一些基本配置,如设置密码、绑定IP地址等。

2. 选择开发语言及相关库

这里以Python为例,Python有丰富的数据库和Redis操作库。

  • 安装MySQL库:使用pip install mysql - connector - python安装MySQL连接库,该库提供了Python与MySQL交互的接口。
  • 安装Redis库:使用pip install redis安装Redis连接库,方便在Python代码中操作Redis。

实现读写分离缓存模式的Python代码示例

1. 连接MySQL和Redis

import mysql.connector
import redis

# 连接MySQL读服务器
mysql_read = mysql.connector.connect(
    host='read_host',
    user='user',
    password='password',
    database='test_db'
)

# 连接MySQL主服务器
mysql_write = mysql.connector.connect(
    host='write_host',
    user='user',
    password='password',
    database='test_db'
)

# 连接Redis
r = redis.Redis(host='redis_host', port=6379, password='redis_password')

在上述代码中,分别建立了与MySQL读服务器、写服务器以及Redis的连接。注意将read_hostwrite_hostredis_host等替换为实际的服务器地址,userpassword替换为真实的数据库和Redis访问凭证。

2. 读操作实现

def get_user_from_cache_or_db(user_id):
    user = r.get(f'user:{user_id}')
    if user:
        return user.decode('utf - 8')

    cursor = mysql_read.cursor()
    cursor.execute(f'SELECT * FROM users WHERE id = {user_id}')
    result = cursor.fetchone()
    if result:
        user_info = f'Name: {result[1]}, Age: {result[2]}'
        r.set(f'user:{user_id}', user_info)
        return user_info
    return None

get_user_from_cache_or_db函数中,首先尝试从Redis缓存中获取用户信息。如果缓存中存在,则直接返回。否则,从MySQL读服务器查询用户信息,查询到后将其存入Redis缓存并返回。如果未查询到,则返回None

3. 写操作实现

def add_user_to_db_and_cache(name, age):
    cursor = mysql_write.cursor()
    cursor.execute('INSERT INTO users (name, age) VALUES (%s, %s)', (name, age))
    mysql_write.commit()
    new_id = cursor.lastrowid

    user_info = f'Name: {name}, Age: {age}'
    r.set(f'user:{new_id}', user_info)
    return new_id

add_user_to_db_and_cache函数实现了将新用户信息插入到MySQL主服务器,并同时更新Redis缓存的功能。先执行MySQL插入操作,获取插入后的自增ID,然后构造用户信息存入Redis。

缓存更新策略优化

1. 常见缓存更新策略

  • 先更新数据库,再更新缓存:这是最直观的策略,在写操作时先修改数据库,再修改缓存。然而,在高并发场景下可能出现问题。例如,线程A更新数据库后,在更新缓存前,线程B读取数据,由于缓存未更新,线程B读取到旧数据,之后线程A更新缓存,导致缓存中的数据与数据库不一致。
  • 先删除缓存,再更新数据库:这种策略在写操作时先删除缓存,再更新数据库。但同样在高并发场景下存在问题。假设线程A删除缓存后,线程B读取数据,发现缓存不存在,从数据库读取旧数据并写入缓存,然后线程A更新数据库,此时缓存中的数据与数据库不一致。
  • 先更新数据库,再删除缓存:目前相对较好的策略。在写操作时先更新数据库,再删除缓存。因为删除操作比更新操作更简单、更快速,且即使在高并发下,短暂的缓存不一致问题也相对容易接受,后续读操作会重新从数据库读取最新数据并更新缓存。

2. 代码实现优化后的缓存更新策略

以更新用户年龄为例:

def update_user_age_in_db_and_cache(user_id, new_age):
    cursor = mysql_write.cursor()
    cursor.execute('UPDATE users SET age = %s WHERE id = %s', (new_age, user_id))
    mysql_write.commit()

    r.delete(f'user:{user_id}')

update_user_age_in_db_and_cache函数中,先更新MySQL数据库中用户的年龄,然后删除对应的Redis缓存键,确保后续读操作会从数据库获取最新数据并重新构建缓存。

缓存穿透、雪崩和击穿问题及解决方案

1. 缓存穿透

  • 问题描述:缓存穿透指的是查询一个不存在的数据,由于缓存中没有,每次都会查询数据库,若有大量这样的请求,会对数据库造成巨大压力,甚至导致数据库崩溃。例如,恶意用户频繁查询不存在的用户ID。
  • 解决方案
    • 布隆过滤器:布隆过滤器是一种概率型数据结构,可以用来判断一个元素是否存在于集合中。在系统初始化时,将数据库中所有存在的键值对通过布隆过滤器进行处理。当有查询请求时,先通过布隆过滤器判断该键是否存在。如果不存在,则直接返回,不再查询数据库。Python中可以使用bitarraymmh3库实现简单的布隆过滤器,示例代码如下:
import bitarray
import mmh3


class BloomFilter:
    def __init__(self, size, hash_count):
        self.size = size
        self.hash_count = hash_count
        self.bit_array = bitarray.bitarray(size)
        self.bit_array.setall(0)

    def add(self, item):
        for i in range(self.hash_count):
            index = mmh3.hash(item, i) % self.size
            self.bit_array[index] = 1

    def lookup(self, item):
        for i in range(self.hash_count):
            index = mmh3.hash(item, i) % self.size
            if not self.bit_array[index]:
                return False
        return True


# 使用示例
bloom = BloomFilter(10000, 5)
keys = ['user:1', 'user:2', 'user:3']
for key in keys:
    bloom.add(key)
if bloom.lookup('user:1'):
    print('可能存在')
else:
    print('一定不存在')
- **缓存空值**:当查询数据库发现数据不存在时,将空值存入Redis缓存,并设置较短的过期时间。这样,下次相同查询直接从缓存返回空值,避免查询数据库。示例代码如下:
def get_user_from_cache_or_db(user_id):
    user = r.get(f'user:{user_id}')
    if user:
        if user == 'null':
            return None
        return user.decode('utf - 8')

    cursor = mysql_read.cursor()
    cursor.execute(f'SELECT * FROM users WHERE id = {user_id}')
    result = cursor.fetchone()
    if result:
        user_info = f'Name: {result[1]}, Age: {result[2]}'
        r.set(f'user:{user_id}', user_info)
        return user_info
    else:
        r.setex(f'user:{user_id}', 60, 'null')  # 设置空值缓存,60秒过期
        return None

2. 缓存雪崩

  • 问题描述:缓存雪崩指的是大量的缓存数据在同一时间过期,导致大量请求直接查询数据库,造成数据库压力瞬间增大,甚至崩溃。例如,系统中缓存的有效期都设置为1小时,1小时后所有缓存同时失效。
  • 解决方案
    • 随机过期时间:在设置缓存过期时间时,不使用固定的过期时间,而是设置一个随机范围的过期时间。例如,将过期时间设置为60 - 120分钟之间的随机值,这样可以避免大量缓存同时过期。示例代码如下:
import random


def get_user_from_cache_or_db(user_id):
    user = r.get(f'user:{user_id}')
    if user:
        return user.decode('utf - 8')

    cursor = mysql_read.cursor()
    cursor.execute(f'SELECT * FROM users WHERE id = {user_id}')
    result = cursor.fetchone()
    if result:
        user_info = f'Name: {result[1]}, Age: {result[2]}'
        expire_time = random.randint(60 * 60, 120 * 60)  # 60 - 120分钟随机过期时间
        r.setex(f'user:{user_id}', expire_time, user_info)
        return user_info
    return None
- **使用二级缓存**:设置一级缓存和二级缓存,一级缓存失效后,先从二级缓存获取数据。二级缓存可以设置较长的过期时间或者不设置过期时间。例如,可以将常用数据在Redis主节点作为一级缓存,在从节点作为二级缓存。

3. 缓存击穿

  • 问题描述:缓存击穿指的是一个热点数据在缓存过期的瞬间,大量并发请求同时查询该数据,由于缓存失效,这些请求都会直接查询数据库,给数据库造成巨大压力。例如,某个热门商品的缓存过期,大量用户同时查询该商品信息。
  • 解决方案
    • 互斥锁:在缓存失效时,使用互斥锁(如Redis的SETNX命令)来保证只有一个线程去查询数据库并更新缓存,其他线程等待。示例代码如下:
import time


def get_user_from_cache_or_db(user_id):
    user = r.get(f'user:{user_id}')
    if user:
        return user.decode('utf - 8')

    lock_key = f'lock:user:{user_id}'
    while not r.set(lock_key, 1, nx=True, ex=10):  # 尝试获取锁,10秒过期
        time.sleep(0.1)

    try:
        cursor = mysql_read.cursor()
        cursor.execute(f'SELECT * FROM users WHERE id = {user_id}')
        result = cursor.fetchone()
        if result:
            user_info = f'Name: {result[1]}, Age: {result[2]}'
            r.setex(f'user:{user_id}', 3600, user_info)
            return user_info
        return None
    finally:
        r.delete(lock_key)  # 释放锁
- **热点数据永不过期**:对于热点数据,不设置过期时间,而是通过后台线程定期更新缓存数据,或者在数据发生变化时主动更新缓存。

性能监控与调优

1. 监控指标

  • Redis监控指标
    • 命中率:通过计算缓存命中次数与总请求次数的比例来衡量。高命中率意味着大部分请求可以从缓存获取数据,减少了数据库压力。在Redis中,可以通过INFO命令获取keyspace_hits(命中次数)和keyspace_misses(未命中次数)来计算命中率,公式为:命中率 = keyspace_hits / (keyspace_hits + keyspace_misses)
    • 内存使用情况:包括已使用内存、内存碎片率等。使用INFO memory命令可以获取相关信息。内存碎片率过高可能影响性能,需要进行优化。
    • QPS(每秒查询率):反映了Redis处理请求的能力,通过监控QPS可以了解系统的负载情况。
  • MySQL监控指标
    • 查询响应时间:通过慢查询日志等工具可以记录查询的执行时间,过长的响应时间可能意味着查询需要优化。
    • CPU和内存使用率:使用系统监控工具(如top、htop等)可以查看MySQL服务器的CPU和内存使用情况,过高的使用率可能导致性能瓶颈。
    • 主从复制延迟:对于读写分离架构,主从复制延迟影响读数据的实时性。可以通过SHOW STATUS命令查看Seconds_Behind_Master参数来了解从服务器落后主服务器的时间。

2. 调优措施

  • Redis调优
    • 优化数据结构:根据实际应用场景选择合适的Redis数据结构。例如,如果需要存储大量有序数据,可以使用Sorted Set;如果只是简单的键值对存储,String即可。
    • 调整缓存淘汰策略:Redis提供了多种缓存淘汰策略,如volatile - lru(在设置了过期时间的键中使用LRU算法淘汰)、allkeys - lru(在所有键中使用LRU算法淘汰)等。根据业务需求选择合适的淘汰策略,以保证缓存的有效性。
    • 优化网络配置:合理设置Redis的绑定IP地址、端口号,以及调整TCP参数(如tcp - keepalive),可以提高网络传输性能。
  • MySQL调优
    • 查询优化:使用EXPLAIN关键字分析查询语句,优化索引使用,避免全表扫描。例如,为经常用于查询条件的字段添加索引。
    • 配置参数调整:根据服务器硬件资源和业务需求,调整MySQL的配置参数,如innodb_buffer_pool_size(InnoDB存储引擎缓冲池大小)、max_connections(最大连接数)等。
    • 主从复制优化:合理设置主从复制的相关参数,如sync_binlog(控制二进制日志刷新到磁盘的频率)、relay_log(从服务器中继日志的路径)等,减少主从复制延迟。

高可用和容灾设计

1. Redis高可用

  • 主从复制:Redis的主从复制是实现高可用的基础。主节点负责写操作,从节点通过复制主节点的数据来实现数据同步。可以通过配置文件中的slaveof参数来设置从节点。例如,在从节点的redis.conf文件中添加slaveof master_ip master_port,重启Redis服务后,从节点就会开始复制主节点的数据。主从复制不仅可以提高读性能,还可以在主节点故障时,手动将从节点提升为主节点,保证服务的可用性。
  • Sentinel(哨兵):Sentinel是Redis官方提供的高可用性解决方案。它可以监控Redis主从节点的健康状态,当主节点出现故障时,自动将从节点提升为主节点,并通知应用程序新的主节点地址。Sentinel本身也是分布式的,可以部署多个Sentinel节点来提高可靠性。部署Sentinel时,需要创建Sentinel配置文件(如sentinel.conf),在其中配置要监控的主节点信息,示例如下:
sentinel monitor mymaster master_ip master_port 2
sentinel down - after - milliseconds mymaster 30000
sentinel failover - timeout mymaster 180000

上述配置中,sentinel monitor指定了要监控的主节点名称(mymaster)、主节点IP地址和端口号,以及判断主节点失效至少需要多少个Sentinel节点同意(2个)。sentinel down - after - milliseconds设置了判断主节点不可用的时间阈值,sentinel failover - timeout设置了故障转移的超时时间。

2. MySQL高可用

  • 主从复制:MySQL的主从复制原理与Redis类似,主服务器将写操作记录到二进制日志中,从服务器通过I/O线程读取主服务器的二进制日志,并通过SQL线程将日志中的操作应用到自身数据库,从而实现数据同步。配置MySQL主从复制时,需要在主服务器的my.cnf文件中开启二进制日志功能,设置server - id等参数,在从服务器同样设置server - id,并通过CHANGE MASTER TO语句配置主服务器信息。例如:
-- 在主服务器my.cnf中添加
log - bin = /var/log/mysql/mysql - bin.log
server - id = 1

-- 在从服务器上执行
CHANGE MASTER TO
    MASTER_HOST='master_ip',
    MASTER_USER='replication_user',
    MASTER_PASSWORD='replication_password',
    MASTER_LOG_FILE='master_binlog_file',
    MASTER_LOG_POS=master_binlog_position;
START SLAVE;
  • MHA(Master High Availability):MHA是一款常用的MySQL高可用性解决方案,它可以在主服务器出现故障时,快速自动地将从服务器提升为主服务器,保证业务的连续性。MHA由管理节点(Manager Node)和数据节点(Data Node)组成,管理节点负责监控和故障转移,数据节点即MySQL主从服务器。部署MHA时,需要在管理节点安装MHA Manager软件包,在数据节点安装MHA Node软件包,并进行相应的配置。例如,在管理节点的配置文件(如app1.cnf)中配置数据节点信息:
[server default]
manager_workdir=/var/log/mha/app1
manager_log=/var/log/mha/app1/manager.log
master_binlog_dir=/var/log/mysql
user=root
password=root_password
ping_interval=1
repl_password=replication_password
repl_user=replication_user

[server1]
hostname=master_ip
candidate_master=1

[server2]
hostname=slave1_ip
candidate_master=1

[server3]
hostname=slave2_ip

上述配置中,[server default]部分设置了一些通用参数,[server1][server2][server3]分别配置了主服务器和从服务器的信息,candidate_master参数指定了哪些从服务器可以被提升为主服务器。

3. 容灾设计

  • 数据备份:定期对MySQL数据库进行备份,可以使用mysqldump命令或者专业的备份工具(如Percona XtraBackup)。对于Redis,可以使用SAVEBGSAVE命令生成RDB快照,或者使用AOF(Append - Only - File)日志记录写操作,以便在故障恢复时重放日志。
  • 异地多活:在不同地理位置部署多个数据中心,每个数据中心都包含完整或部分的业务数据。当某个数据中心出现故障时,其他数据中心可以继续提供服务,保证业务的连续性。在实现异地多活时,需要考虑数据同步、网络延迟、负载均衡等问题。例如,可以使用MySQL的多源复制功能实现不同数据中心之间的数据同步,使用DNS或负载均衡器将用户请求分配到不同的数据中心。

通过以上对Redis与MySQL读写分离缓存模式的实战优化,从基础概念、代码实现、缓存策略、问题解决、性能监控到高可用和容灾设计等方面进行了详细阐述,希望能帮助开发者构建高效、稳定、可靠的数据访问架构。