PHP项目缓存数据如何同步数据库

wen PHP项目 27

PHP项目缓存数据如何同步数据库:从原理到落地的完整指南

目录导读

  1. 为什么会出现缓存与数据库数据不一致?
  2. 常见缓存同步策略对比与选型
  3. PHP项目中的五种核心同步方案
  4. 实战:基于Redis+MySQL的缓存同步代码示例
  5. 如何避免缓存穿透、击穿、雪崩?
  6. 常见问题问答(QA)
  7. 总结与最佳实践建议

为什么会出现缓存与数据库数据不一致?

在PHP高并发项目中,为了提升读取性能,我们通常使用Redis、Memcached等缓存存储频繁访问的数据,但缓存本质是数据库的“快照副本”,当数据库数据发生更新(增、删、改)时,如果缓存没有同步更新,就会出现缓存脏数据,表现为用户看到的数据与真实数据库不一致。

PHP项目缓存数据如何同步数据库

典型场景:

  • 用户修改个人信息,缓存中的旧数据未被清除
  • 电商库存扣减时,缓存中的库存数量滞后于数据库
  • 文章阅读量频繁更新,缓存与数据库数值偏差

核心矛盾:数据库是持久化存储,缓存是临时快照,两者必须通过某种机制保持最终一致性。


常见缓存同步策略对比与选型

策略名称 原理 优点 缺点 适用场景
Cache-Aside 读时回写,写时删除 实现简单 可能产生短时不一致 大多数业务
Read-Through 缓存层自动加载数据库数据 对业务透明 需依赖缓存中间件 通用场景
Write-Through 写操作同时写入缓存和数据库 强一致性 写延迟增加 对一致性要求极高
Write-Behind 先写缓存,异步批量写数据库 写性能高 存在数据丢失风险 日志、计数场景
双删策略 写数据库前后各删一次缓存 提升一致性概率 实现复杂度高 高并发更新场景

推荐:对于绝大多数PHP项目,Cache-Aside模式 + 延迟双删是性价比最高的方案。


PHP项目中的五种核心同步方案

手动清除缓存(Cache-Aside经典模式)

// 更新数据库后立即删除缓存
$pdo->exec("UPDATE users SET name='张三' WHERE id=1");
$redis->del("user:1");

缺点:高并发下,一个线程删除缓存后,另一个线程可能读旧数据并重建缓存。

延迟双删(解决缓存重建竞争)

$pdo->exec("UPDATE users SET name='张三' WHERE id=1");
$redis->del("user:1");
// 延迟100ms再次删除
\Swoole\Coroutine::sleep(0.1);
$redis->del("user:1");

原理:第二次删除覆盖了第一次删除到重建缓存之间的时间差。

消息队列异步同步

使用RabbitMQ或Redis Stream,将更新事件发送到队列:

// 写操作
$pdo->exec("UPDATE users SET name='张三' WHERE id=1");
$redis->lPush("cache:sync:queue", json_encode(['table'=>'users', 'id'=>1]));
// 消费者进程
while($msg = $redis->rPop("cache:sync:queue")) {
    $data = json_decode($msg, true);
    $redis->del("{$data['table']}:{$data['id']}");
}

数据库Binlog监听

通过Canal或Maxwell监听MySQL变更日志,实时推送至Redis:

  • 优点:与业务代码解耦
  • 缺点:需要额外部署中间件

缓存过期时间兜底

即使同步失败,设置合理过期时间(如1小时),保证最终一致性:

$redis->setex("user:1", 3600, $data); // 1小时自动过期

实战:基于Redis+MySQL的缓存同步代码示例

以下是一个完整的用户信息更新同步函数:

class UserCacheSync {
    private $pdo;
    private $redis;
    public function updateUser(int $id, array $data): bool {
        $this->pdo->beginTransaction();
        try {
            // 1. 更新数据库
            $sql = "UPDATE users SET name=:name, email=:email WHERE id=:id";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([
                ':name' => $data['name'],
                ':email' => $data['email'],
                ':id' => $id
            ]);
            // 2. 第一次删除缓存
            $cacheKey = "user:{$id}";
            $this->redis->del($cacheKey);
            $this->pdo->commit();
            // 3. 延迟300ms(根据业务调整)后再次删除
            usleep(300000); // 300ms
            $this->redis->del($cacheKey);
            return true;
        } catch (\Exception $e) {
            $this->pdo->rollBack();
            // 记录日志,告警
            Log::error("User update failed: " . $e->getMessage());
            return false;
        }
    }
}

关键点

  • 事务保证数据库更新原子性
  • 两次删除间隔根据网络IO调整
  • 失败时记录日志并人工介入

如何避免缓存穿透、击穿、雪崩?

在同步缓存的过程中,这三个问题同样需要防范:

问题 现象 解决方案
穿透 请求不存在的数据,直接打到数据库 布隆过滤器 + 缓存空对象(短过期)
击穿 热点key突然失效,大量请求直击数据库 互斥锁(SETNX)重建缓存
雪崩 大量key同时过期 过期时间加随机值 + 多级缓存

代码示例:使用互斥锁防止缓存击穿

$cacheKey = "hot:product:1";
$data = $redis->get($cacheKey);
if (!$data) {
    if ($redis->setnx("lock:{$cacheKey}", 1)) {
        $redis->expire("lock:{$cacheKey}", 5);
        $data = $pdo->query("SELECT * FROM products WHERE id=1");
        $redis->setex($cacheKey, 3600, $data);
        $redis->del("lock:{$cacheKey}");
    } else {
        usleep(10000);
        return $this->getProduct(1); // 重试
    }
}

常见问题问答(QA)

Q1: 延迟双删中的延迟时间如何确定?

A: 通常取业务数据库主从同步延迟的2倍,例如主从延迟平均100ms,则延迟设为200-300ms,也可以通过测量从执行删除到查询重建的时间估算。

Q2: 如果缓存删除失败该怎么办?

A: 采用失败重试机制:

  • 记录删除失败的key到本地文件或独立队列
  • 定时任务重新删除
  • 使用Redis本身的消息队列确保最终删除

Q3: 缓存同步可以做到强一致性吗?

A: 在分布式系统中,CAP理论限制无法达到严格强一致性,可以通过以下方式近似:

  • 使用分布式锁(如RedLock)保护读写操作
  • 采用数据库乐观锁(版本号)在缓存中校验
  • 接受秒级延迟(最终一致性)

Q4: 方案一(手动删除)和方案三(消息队列)如何选择?

A:

  • 并发量<1000QPS,数据一致性要求一般:方案一即可
  • 并发量>5000QPS,需要解耦:方案三更合适
  • 对数据一致性要求极高:方案二(延迟双删)+ 方案五(短过期)组合

总结与最佳实践建议

核心原则

  1. 缓存是数据库的副本,不是源数据
  2. 写操作先更新数据库,后删除缓存
  3. 接受最终一致性,通过过期时间兜底

推荐技术栈

  • PHP框架:Laravel/Lumen 自带缓存标签功能,简化同步
  • Redis:使用Lua脚本原子化删除操作
  • 监控:redis-cli monitor + slowlog 追踪缓存操作

性能调优

  • 尽量小批量删除,避免DEL大key阻塞
  • 对LIST结构缓存,使用UNLINK替代DEL(异步释放内存)
  • 冷数据主动过期,缓存中只保留热点数据

最后提醒

不要试图让缓存和100%实时一致,在99.9%的场景中,最终一致性配合合理的监控告警,就是最优秀的工程方案,投入过多的成本追求完全一致,往往得不偿失。


延伸阅读

  • Redis官方缓存淘汰策略文档
  • MySQL Binlog同步中间件Canal实战
  • 布隆过滤器在PHP中的使用

(全文完)

抱歉,评论功能暂时关闭!