背景:公司旧项目,最初访问量不多,单机部署的。后来,访问量上来了,有阵子很卡,公司决定横向扩展,后端代码部署了三台服务器。部署调整后,有用户反馈,一个订单支付了三次。
问题分析:
通过对项目日志分析,问题应该出现在,使用Redis做分布式锁,没有做到原子性操作。判断键是否存在和设置键及有效期是分两步来的,服务器卡的时候,放大了这两步操作的时间,导致了问题产生。
问题解决:
得优化这部分代码,得采用Redis调用lua脚本,实现操作的原子性。查了各种博客及Redis官方文档,推荐了ronnylt/redlock-php这个扩展,进来发现这个扩展比较老旧了,支持PHP版本太老了。
通过搜索redlock-php发现signe/redlock-php扩展应该可以满足公司项目需求。
PHP分布式测试:
本地部署了两套PHP接口代码,环境如下:
ThinkPHP8.0
PHP8
composer require ronnylt/redlock-php
测试接口代码如下:
<?php
declare (strict_types=1);namespace app\controller;use RedLock\RedLock;class Index
{public function index(){$servers = [['127.0.0.1', 6379, 0.01],];$redLock = new RedLock($servers);$lock = $redLock->lock('my_resource_name', 200000);// $lock是一个数组,说明抢到临界资源,可以执行业务;上锁失败是falseif ($lock) {// 执行业务逻辑echo "执行业务代码...<br/>";// 业务执行完毕,可以进行解锁操作$redLock->unlock($lock);} else {echo "未抢到临界资源,继续等待...<br/>";}}
}
测试时注释了解锁时间,把加锁时间设置成了200秒,测试结果符合预期,下面是测试过程截图
符合预期后,把这扩展安装到现有项目,优化了现有代码,测试上线,这里记录下,防止下回碰到类似问题又去从零梳理解决此类问题。
小结下流程:
1、PHP项目安装扩展
composer require ronnylt/redlock-php
2、分布式锁实现代码示例
<?php
declare (strict_types=1);namespace app\controller;use RedLock\RedLock;class Index
{public function index(){$servers = [['127.0.0.1', 6379, 0.01],];$redLock = new RedLock($servers);$lock = $redLock->lock('my_resource_name', 200000);// $lock是一个数组,说明抢到临界资源,可以执行业务;上锁失败是falseif ($lock) {// 执行业务逻辑echo "执行业务代码...<br/>";// 业务执行完毕,可以进行解锁操作// $redLock->unlock($lock);} else {echo "未抢到临界资源,继续等待...<br/>";}}
}
概念补充:
分布式锁:
保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
实现分布式锁一般有三种实现方式:
1. 数据库乐观锁
2. 基于Redis的分布式锁
3. 基于Zookeeper的分布式锁
确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
1. 互斥性。在任意时刻,只有一个客户端能持有锁。2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。3. 安全性。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。4. 容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。