Yii 2: The Fast, Secure and Professional PHP Framework 分布式锁实现:Redis 与 Memcached
在分布式系统中,多个进程或服务器同时操作共享资源时,很容易出现“竞态条件”(Race Condition)。比如多个用户同时购买最后一件商品、并发更新同一条数据等场景,都可能导致数据不一致。Yii 2 框架提供了强大的分布式锁(Mutex)机制,帮助开发者轻松解决这类问题。本文将重点介绍如何在 Yii 2 中使用 Redis 和 Memcached 实现分布式锁,确保系统在高并发环境下的数据安全。
分布式锁基础:Yii 2 Mutex 组件
Yii 2 的分布式锁功能基于 yii\mutex\Mutex 组件实现,该组件继承自 yii\base\Component,支持属性、事件和行为三大核心特性。Mutex 组件通过“锁”机制保证并发操作的互斥性,其核心方法包括:
acquire($name, $timeout = 0): 获取锁,返回true表示成功获取release($name): 释放锁isAcquired($name): 检查当前进程是否持有锁
锁的自动释放机制
Mutex 组件默认开启 autoRelease = true,会在脚本执行结束时自动释放所有未释放的锁,避免死锁。这一机制通过 register_shutdown_function 实现,确保即使程序异常退出也能清理资源:
// framework/mutex/Mutex.php 自动释放逻辑
public function init()
{
if ($this->autoRelease) {
$locks = &$this->_locks;
register_shutdown_function(function () use (&$locks) {
foreach ($locks as $lock) {
$this->release($lock);
}
});
}
}
Yii 2 分布式锁实现方式
Yii 2 官方提供了多种 Mutex 实现,常见的有数据库锁(DbMutex)、文件锁(FileMutex)等。虽然环境中未直接提供 Redis 和 Memcached 锁的实现,但我们可以基于官方组件扩展,或使用社区成熟方案。以下是两种主流分布式锁的实现方案:
方案一:基于 Redis 的分布式锁
Redis 因其高性能和原子操作特性,是实现分布式锁的理想选择。Yii 2 可通过 yii2-redis 扩展结合 Mutex 组件实现 Redis 锁。
1. 安装 Redis 扩展
composer require yiisoft/yii2-redis
2. 配置 Redis 组件
在 config/main.php 中添加 Redis 组件配置:
'components' => [
'redis' => [
'class' => 'yii\redis\Connection',
'hostname' => 'localhost',
'port' => 6379,
'database' => 0,
],
'mutex' => [
'class' => 'yii\redis\Mutex', // 假设存在该实现
'redis' => 'redis', // 引用 Redis 组件
'ttl' => 30, // 锁超时时间(秒)
],
],
3. Redis 锁核心实现逻辑
Redis 锁通过 SET NX PX 命令实现(不存在则设置,带过期时间),确保原子性:
// 简化的 Redis 锁实现
protected function acquireLock($name, $timeout = 0)
{
$redis = $this->redis;
$key = $this->generateLockKey($name);
$expire = $this->ttl * 1000; // 毫秒
$token = uniqid(); // 生成唯一令牌,用于释放锁
// 使用 SET NX PX 命令获取锁
$result = $redis->executeCommand('SET', [$key, $token, 'NX', 'PX', $expire]);
if ($result === 'OK') {
// 记录令牌,释放时验证
$this->_lockTokens[$name] = $token;
return true;
}
// 处理超时等待逻辑(简化版)
if ($timeout > 0) {
// 循环重试,直到超时
$end = time() + $timeout;
while (time() < $end) {
usleep(100000); // 100ms 重试一次
$result = $redis->executeCommand('SET', [$key, $token, 'NX', 'PX', $expire]);
if ($result === 'OK') {
$this->_lockTokens[$name] = $token;
return true;
}
}
}
return false;
}
// 释放锁时验证令牌,避免误删其他进程的锁
protected function releaseLock($name)
{
if (!isset($this->_lockTokens[$name])) {
return false;
}
$redis = $this->redis;
$key = $this->generateLockKey($name);
$token = $this->_lockTokens[$name];
// 使用 Lua 脚本保证释放锁的原子性
$script = <<<LUA
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
LUA;
$result = $redis->executeCommand('EVAL', [$script, 1, $key, $token]);
unset($this->_lockTokens[$name]);
return $result === 1;
}
方案二:基于 Memcached 的分布式锁
Memcached 作为经典的内存缓存系统,也可用于实现分布式锁。其 add 命令(仅在键不存在时设置)天然支持锁的获取逻辑。
1. 配置 Memcached 组件
'components' => [
'memcache' => [
'class' => 'yii\caching\MemCache',
'servers' => [
[
'host' => 'localhost',
'port' => 11211,
'weight' => 100,
],
],
],
'mutex' => [
'class' => 'app\components\MemCacheMutex', // 自定义实现
'memcache' => 'memcache',
'ttl' => 30,
],
],
2. Memcached 锁核心实现
// 简化的 Memcached 锁实现
protected function acquireLock($name, $timeout = 0)
{
$memcache = $this->memcache;
$key = $this->generateLockKey($name);
$value = uniqid(); // 存储唯一值,避免误释放
// 使用 add 命令获取锁(键不存在时设置成功)
if ($memcache->add($key, $value, 0, $this->ttl)) {
$this->_lockValues[$name] = $value;
return true;
}
// 超时等待逻辑
if ($timeout > 0) {
$end = time() + $timeout;
while (time() < $end) {
usleep(100000);
if ($memcache->add($key, $value, 0, $this->ttl)) {
$this->_lockValues[$name] = $value;
return true;
}
}
}
return false;
}
protected function releaseLock($name)
{
if (!isset($this->_lockValues[$name])) {
return false;
}
$memcache = $this->memcache;
$key = $this->generateLockKey($name);
$value = $this->_lockValues[$name];
// 验证值后删除,避免释放其他进程的锁
$currentValue = $memcache->get($key);
if ($currentValue === $value) {
$memcache->delete($key);
unset($this->_lockValues[$name]);
return true;
}
return false;
}
分布式锁实战:商品秒杀场景
以电商秒杀场景为例,使用 Yii 2 Mutex 组件确保库存操作的原子性:
// 控制器中的秒杀逻辑
public function actionSeckill($productId)
{
$mutex = Yii::$app->mutex;
$lockName = "seckill_{$productId}";
try {
// 获取锁,最多等待 3 秒
if ($mutex->acquire($lockName, 3)) {
// 查询库存(需加行锁)
$product = Product::find()->where(['id' => $productId])->lockForUpdate()->one();
if ($product->stock <= 0) {
return $this->asJson(['status' => 'error', 'msg' => '商品已抢完']);
}
// 扣减库存
$product->stock -= 1;
$product->save(false);
// 创建订单
$order = new Order();
$order->product_id = $productId;
$order->user_id = Yii::$app->user->id;
$order->save();
return $this->asJson(['status' => 'success', 'order_id' => $order->id]);
} else {
return $this->asJson(['status' => 'error', 'msg' => '系统繁忙,请稍后再试']);
}
} finally {
// 手动释放锁(可选,autoRelease 会自动处理)
if ($mutex->isAcquired($lockName)) {
$mutex->release($lockName);
}
}
}
Redis 与 Memcached 锁的对比与选型
| 特性 | Redis 锁 | Memcached 锁 |
|---|---|---|
| 原子性 | 支持(SET NX PX/Lua 脚本) | 基础支持(add 命令) |
| 死锁预防 | 支持超时自动释放 | 支持超时自动释放 |
| 集群支持 | 原生支持(Redlock 算法) | 依赖客户端实现(如一致性哈希) |
| 性能 | 极高(单线程模型,百万级 QPS) | 高(多线程模型,十万级 QPS) |
| 数据持久化 | 支持(AOF/RDB) | 不支持(内存数据库) |
选型建议:
- 分布式系统优先选择 Redis,支持更完善的锁机制和集群方案
- 简单缓存场景可使用 Memcached,部署简单、资源占用低
- 金融等核心场景建议结合数据库事务 + 分布式锁双重保障
最佳实践与注意事项
- 设置合理的锁超时时间:根据业务执行时间设置
ttl,避免锁持有过久导致并发阻塞 - 避免长时间持有锁:锁内逻辑尽量精简,非核心操作移至锁外执行
- 锁的重试机制:结合 RetryAcquireTrait 实现自动重试,提高锁获取成功率
- 防死锁设计:除超时机制外,可引入“看门狗”线程续期锁超时时间
- 监控与告警:通过 Prometheus 等工具监控锁竞争情况,设置告警阈值
总结
Yii 2 的 Mutex 组件为分布式锁提供了灵活的抽象层,结合 Redis 或 Memcached 可实现高性能、高可靠的分布式锁方案。在实际开发中,需根据业务场景选择合适的锁实现,并遵循最佳实践避免死锁、性能瓶颈等问题。通过本文介绍的方法,开发者可快速在 Yii 2 项目中集成分布式锁,保障高并发场景下的数据一致性。
若需深入了解 Yii 2 组件开发,可参考官方文档 概念:组件 和 Mutex 源码。对于分布式锁的高级应用,推荐研究 Redlock 算法及 Yii 社区扩展(如 yiisoft/yii2-redis)。
希望本文能帮助你在 Yii 2 项目中轻松应对并发挑战,构建更稳定的分布式系统!如果你有任何疑问或实践经验,欢迎在评论区分享交流。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



