PHP高并发场景实战:缓存失效、幂等性与超卖问题解决
在高并发业务场景中,PHP应用常面临缓存雪崩引发的数据库压力、网络延迟导致的重复提交以及秒杀活动中的库存超卖问题。本文将详细解析三种实战解法:一是配置Redis键空间通知处理缓存失效,二是利用setnx实现接口幂等性控制,三是通过数据库乐观锁防止超卖,帮助开发者构建更健壮的后台系统。
1. 利用Redis通知处理缓存失效
当某个接口启用数据缓存且设置较短过期时间时,缓存集中失效会导致瞬间大量请求直连数据库,可能拖垮服务。除了常见的随机化过期时间以分散压力外,若需执行特定的业务逻辑(如重新组装大表数据或触发状态变更),可订阅Redis的键失效事件频道,实现“感知式”刷新。
第一步:开启Redis键空间通知
默认情况下Redis不发送键过期通知,需在服务端进行配置。对于非持久化测试环境,可在终端执行以下命令临时开启:
config set notify-keyspace-events Ex
在生产环境的配置文件 redis.windows-service.conf(Linux通常为 redis.conf)中找到对应行,去掉注释并设置为:
notify-keyspace-events "Ex"
其中 E 代表事件通知,x 代表过期事件。如需监听删除操作可设为 gxdel 等组合参数。修改后重启Redis生效。
第二步:验证键失效频道的接收情况
打开一个新的Redis客户端终端,订阅0号数据库的所有库过期频道,观察是否有消息输出:
subscribe __keyevent@0__:expired
在另一个终端中,设置一个30秒后过期的测试键值对:
setex test_key_name 30 ceshi
等待过期后,查看订阅窗口是否收到了 test_key_name 的键名推送。此步骤用于确认配置是否正确及网络连接是否通畅。
第三步:编写PHP CLI脚本实现监听
PHP代码需通过 php-cli 模式运行,保持长连接监听。以下脚本实现了通用消息订阅和Key失效事件订阅两类功能。
<?php
// 禁止脚本超时
set_time_limit(0);
ini_set('default_socket_timeout', -1);
class RedisSubscribe {
protected $config = [
'host' => '127.0.0.1',
'password' => '', // 如果有密码请填入
];
protected $redis;
public function __construct() {
try {
$this->redis = new \Redis();
// 使用pconnect保持长连接
$this->redis->pconnect($this->config['host'], empty($this->config['password']) ? 6379 : $this->config['password']);
} catch (\Exception $e) {
echo "Redis连接失败:".$e->getMessage().PHP_EOL;
}
}
// 普通Pub/Sub频道订阅示例
public function normalSubscribe($channelName = 'test') {
$this->redis->subscribe([$channelName], function ($redis, $channel, $msg) {
echo "收到消息 - Channel: {$channel}, Message: {$msg}".PHP_EOL;
file_put_contents('subscribe.log', "\n-{$msg}-\n", FILE_APPEND);
});
}
// Key过期事件订阅
public function keyNotify() {
echo "开始监听Key失效事件...".PHP_EOL;
// 监听所有数据库的过期事件,也可指定 @0: 为仅0号库
$channel = '__keyevent@*:expired';
$this->redis->subscribe([$channel], function ($redis, $channel, $msg) {
echo "Key已失效: {$msg}".PHP_EOL;
file_put_contents('expire.log', "\n-{$msg}-\n", FILE_APPEND);
// 此处可根据$msg进行业务逻辑处理,例如清空对应的关联缓存或触发重算
});
}
}
// 启动监听
try {
$sub = new RedisSubscribe();
$sub->keyNotify();
} catch (\Exception $e) {
echo $e->getMessage();
}>
第四步:业务集成方案
- 守护进程部署:将该PHP脚本作为常驻进程运行(推荐使用Supervisor或Systemd管理),确保持续监听。
- 策略映射:在项目中维护一份全局缓存键名列表。监听脚本接收到Key名后,匹配该列表中的规则ID,从而决定执行哪种刷新逻辑。
- 应用扩展:此机制不仅可用于缓存预热,还可替代部分低频定时任务(Job)。例如将订单设置为带过期时间的Key,Key失效即表示订单超时未支付,此时直接在回调中将数据库订单状态更新为“已关闭”。
2. 基于Redis SetNX的接口幂等性控制
在高并发注册或提交场景中,网络抖动导致用户重复点击,或上游重试机制引发多次请求,极易产生重复数据。由于数据库查询存在间隙,简单的 `SELECT ... WHERE` 检查无法阻挡并发插入。利用Redis SETNX 命令的原子性可实现分布式锁拦截。
核心原理与代码实现
在执行插入逻辑前,先尝试获取一个以业务条件(如手机号+时间戳)生成的唯一Key。如果 setnx 返回1,说明成功加锁,继续执行数据库操作;如果返回0,说明请求已被拦截或正在进行中,直接返回结果。
/**
* 高并发下防止重复提交/插入数据的幂等性控制
*/
public function preventRepeatSubmit() {
$redis = Redis::db(0); // 获取Redis实例
// 构造唯一标识:当前精确到秒的时间 + 随机数
$lockKey = date('YmdHis', time()) . mt_rand(1000, 9999);
// 1. 尝试获取锁,有效期30秒(防止死锁)
if (!$redis->setnx($lockKey, 1)) {
return json(['code'=>2, 'msg'='处理中,请勿重复提交']);
}
$redis->expire($lockKey, 30);
try {
// 2. 二次校验数据库是否存在(双检锁机制更安全)
// 注意:实际生产建议直接使用 unique index 约束,代码层面做预防
$exist = Db::name('ztest')->where(['no'=>$lockKey])->find();
if ($exist) {
return json(['code'=>2, 'msg'='数据已存在']);
}
// 3. 执行业务逻辑写入
$data = [
'no' => $lockKey,
'tab_num' => 2,
'stock' => 20,
'create_time' => time()
];
$res = Db::name('ztest')->insertGetId($data);
if ($res) {
return json(['code'=>1, 'msg'='添加数据成功']);
}
} catch (\Exception $e) {
// 发生异常时可在此处记录日志
return json(['code'=>2, 'msg'='服务器内部错误']);
}
}
该方法适用于有明确唯一字段或可生成唯一凭证的业务场景。配合前端禁用按钮,能极大降低无效请求率。
3. 数据库乐观锁解决库存超卖
电商秒杀场景中,多个用户同时抢购同一商品可能导致库存变为负数。悲观锁会阻塞大量请求影响性能,因此推荐在PHP+MySQL架构中使用乐观锁(Optimistic Lock)进行更新控制。
实现逻辑
- 版本控制:表中增加
version字段,每次更新时自增。 - CAS机制:更新语句中加入
WHERE version = old_version。只有当数据库中的版本号与查询出来的一致时,更新才成功,影响行数才会为1。 - 无符号限制:库存字段设为无符号(unsigned),即使并发极高,减至0后也不会变负,起到最后一道防线作用。
PHP乐观锁处理代码
public function buyItem() {
// 1. 开启事务
Db::startTrans();
try {
// 2. 查询当前库存和版本号
$item = Db::name('ztest')->where(['id'=>25])->field('stock,version')->find();
if (!$item || $item['stock'] <= 0) {
throw new \Exception('已售罄', 302);
}
$oldVersion = $item['version'];
// 3. 更新库存,要求版本号必须是刚才查出来的值
// 这里的版本号一旦比对不上,update受影响行数就是0
$updateRes = Db::name('ztest')
->where(['id'=>25, 'version'=>$oldVersion])
->update([
'stock' => [$item['stock']-1],
'version' => [$oldVersion+1]
]);
// 4. 判断更新是否成功
if ($updateRes === false || $updateRes === 0) {
// 版本号不一致,说明并发冲突,事务回滚重试或提示稍后
Db::rollback();
throw new \Exception('系统繁忙,请重试', 303);
}
// 5. 提交事务
Db::commit();
return "购买成功";
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
总结
以上便是关于PHP在应对高并发高负载挑战时的三种典型场景及其解决方法。合理运用Redis的消息机制与原子指令,配合数据库层面的乐观锁设计,能有效提升系统的稳定性与数据的一致性。以上就是PHP高并发高负载下的3种实战场景解决方法示例的详细内容,更多关于PHP高并发优化技巧的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













