ci框架session模块在高并发下因redis锁机制缺陷导致数据覆盖,ci3/ci4均存在非原子“检查→设置”漏洞;ci4虽引入重试但未根除竞争;修复方案包括改用setnx、禁用自动锁并手动原子加锁。

CI框架Session模块在高并发场景下因锁机制设计缺陷,导致多个请求同时写入session时数据被覆盖或丢失,用户登录态频繁失效、购物车商品随机消失、表单提交后状态重置等问题集中爆发。
CI3/CI4 Redis驱动中的锁竞争漏洞
CI3和CI4的Redis session驱动均未正确使用原子操作获取锁,而是依赖非原子的“检查→设置”两步逻辑,造成并发请求同时通过ttl判断并成功写入lock_key。
第一步:查看当前锁key的TTL值 → 若返回-2(key不存在)或正值,直接进入下一步;【此时两个并发请求都看到-2,都会继续执行set操作】
第二步:调用redis::set(lock_key, timestamp, ['ex'=>300]) → 该命令不具备排他性,无法阻止第二个请求覆盖第一个请求的锁。
结果是两个请求都认为自己持有了锁,同时对session数据进行读-改-写,最终仅最后一次写入生效,中间修改彻底丢失。
CI4中已修复但仍有隐患的循环等待逻辑
CI4后续版本将锁获取逻辑改为带重试的循环,但未从根本上解决竞争条件:
方法一:使用redis::setNx()替代set()
redis::setNx(lock_key, timestamp)仅在key不存在时才写入并返回true,否则返回false——这是真正具备原子性的加锁原语。
方法二:强制启用EXPIRE+SETNX组合(需手动封装)
先执行redis::setNx(lock_key, '1'),再立即执行redis::expire(lock_key, 300),两次操作必须原子执行;若中间被其他请求插入,则锁可能失效。
【注意:PHP Redis扩展的pipeline或transaction无法保证跨命令原子性,此组合仍存在微小窗口期】
绕过锁缺陷的临时加固方案
步骤一:禁用Redis驱动的自动锁机制,在配置中设置 $config['sess_locking'] = FALSE;
步骤二:将session写操作集中到单一入口,例如所有更新都通过$this->session->set_userdata(['cart'=>$new_cart])统一触发,避免分散调用→write();
步骤三:在关键业务逻辑前手动加锁,使用redis::set($lock_key, time(), ['nx','ex'=>30]) → 这是Redis 2.6.12+支持的原子加锁命令,失败则sleep(50ms)后重试,最多尝试10次。
步骤四:session读操作无需加锁,但读取后若需修改,必须先完成步骤三的加锁流程,再读→改→写→释放锁(del $lock_key)。











