thinkphp5中基于redis的分布式锁实现方案
1.首先先封装一个原生redis客户端的类
<?php
//Cache类的set方法并不直接支持Redis的SET命令的所有选项,如NX和EX。因此,我们需要使用更底层的Redis客户端来实现这一点。
namespace app\common\lib;
class RedisDistributedLock
{
private $client;
private $key;
private $timeout; // 锁的超时时间,单位秒,结合场景而定
private $isWaiting; // 是否等待,默认是0,不等待
private $retryInterval; // 重试间隔,单位毫秒,默认1000是1秒,一般默认1000就行,整好是1秒
public function __construct($key, $timeout = 30, $isWaiting = 0, $retryInterval = 1000)
{
$this->key = $key;
$this->timeout = $timeout;
$this->isWaiting = $isWaiting;
$this->retryInterval = $retryInterval;
$redis = new \Redis();
try {
$redis->connect(config("session")['host'], config("session")['port']);
if (!empty(config("session")['password'])) {
$redis->auth(config("session")['password']); // 密码验证
}
$this->client = $redis;
} catch (\Exception $e) {
throw new \Exception("Redis 连接或认证失败: " . $e->getMessage());
}
}
public function acquire()
{
if ((bool)$this->isWaiting) {
$startTime = microtime(true);
do {
if ($this->tryAcquire()) {
return true;
}
usleep($this->retryInterval * 1000); // 等待一段时间再试
} while (microtime(true) - $startTime < $this->timeout);
throw new \Exception('无法获取锁');
} else {
return $this->tryAcquire();
}
}
public function release()
{
try {
return $this->client->del($this->key);
} catch (\Exception $e) {
throw new \Exception("释放锁失败: " . $e->getMessage());
}
}
private function tryAcquire()
{
try {
return (bool)$this->client->set($this->key, 1, ['nx', 'ex' => $this->timeout]);
} catch (\Exception $e) {
throw new \Exception("尝试获取锁失败: " . $e->getMessage());
}
}
public function getTtl()
{
try {
$ttl = $this->client->ttl($this->key);
if ($ttl === -1) {
throw new \Exception("锁不存在或没有设置过期时间");
}
return $ttl;
} catch (\Exception $e) {
throw new \Exception("获取锁的剩余生存时间失败: " . $e->getMessage());
}
}
}
2.测试的demo方法
<?php
namespace app\index\controller;
use think\Controller;
use think\Db;
use think\Request;
use think\Session;
use think\Cache;
use \think\Env;
use lizengbang\Shorturl;
use think\Image;
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;
use think\Response;
use think\Queue;
use app\middleware\LimitRequestByAppid;
use app\common\lib\RedisDistributedLock;
class TestController extends Controller
{
public function redis_lock2()
{
// redis锁,避免短时间重复点击导出
$lock_key = 'dao_chu_order_son_lock2';
$lock_ttl = 10; // 锁的超时时间(秒)
// 记录开始时间
$start_time = microtime(true);
// 创建锁对象
$lock = new RedisDistributedLock($lock_key, $lock_ttl);
// 尝试获取锁,这里有阻塞吗
$acquired = $lock->acquire();
var_dump($acquired);
if (!$acquired) {
error_tips('导出操作正在进行中,请稍后再试!');
die;
}
try {
echo "我是业务内容";
sleep(4);
} finally {
// 记录结束时间
$end_time = microtime(true);
$execution_time = $end_time - $start_time;
// 释放锁
$deleted = $lock->release();
var_dump($deleted); // 检查锁是否被成功删除
// 输出执行时间
echo "执行时间: " . $execution_time . " 秒";
}
}
public function redis_lock22()
{
// redis锁,避免短时间重复点击导出
$lock_key = 'dao_chu_order_son_lock2';
$lock_ttl = 10; // 锁的超时时间(秒)
// 记录开始时间
$start_time = microtime(true);
// 创建锁对象
$lock = new RedisDistributedLock($lock_key, $lock_ttl);
// 尝试获取锁,这里有阻塞吗
$acquired = $lock->acquire();
var_dump($acquired);
if (!$acquired) {
error_tips('导出操作正在进行中,请稍后再试!');
die;
}
try {
echo "我是业务内容";
} finally {
// 记录结束时间
$end_time = microtime(true);
$execution_time = $end_time - $start_time;
// 释放锁
$deleted = $lock->release();
var_dump($deleted); // 检查锁是否被成功删除
// 输出执行时间
echo "执行时间: " . $execution_time . " 秒";
}
}
}
3.在项目中实际使用的案例
//导出订单操作方法1
public function ajax_do_daochu_order1()
{
//权限校验, 第二个参数是否ajax, 1是ajax,0不是ajax
$basekeynum = session('cn_accountinfo.basekeynum');
check_auth(request()->controller() . '/orderlist_shenhe', 1);
$request = Request::instance();
$param = $request->param();
$clientkeynum = $basekeynum;
// redis锁,避免短时间重复点击导出,锁一定要靠上,从获取数据之前就锁住,要不然可能还重复
$lock_key = $request->action() . '_' . $clientkeynum;
$lock_ttl = 60; // 锁的超时时间(秒)
$lock = new RedisDistributedLock($lock_key, $lock_ttl);
// 尝试获取锁,这里有阻塞吗
$acquired = $lock->acquire();
if (!$acquired) {
error_tips("导出操作正在进行中,请稍" . $lock->getTtl() . "秒后再试!");
die;
}
//开启事务
$trans_result = true;
Db::startTrans();
try {
//sleep(3);
$keynum = isset($param['keynum']) == true ? $param['keynum'] : '-1';
$field = isset($param['field']) ? $param['field'] : '';
$keyword = isset($param['keyword']) ? $param['keyword'] : '';
$between_time = isset($param['between_time']) ? $param['between_time'] : '';
$orderby = isset($param['orderby']) ? $param['orderby'] : 'desc';
$is_print = isset($param['is_print']) ? $param['is_print'] : '-1';
$where = "1=1 and clientkeynum='$basekeynum' and order_status='1' and is_daochu='0' and (channels ='' or (channels !='' and third_sync_sta='1'))";
//订单状态
if ($keynum != '' && $keynum != '-1') {
$where .= " and merchantkeynum='$keynum' ";
//订单状态
}
if ($field == 'order_sn') {
$where .= " and order_sn like '%$keyword%' ";
} elseif ($field == 'goodname') {
$where .= " and goodname like '%$keyword%' ";
} elseif ($field == 'phone') {
$where .= " and phone like '%$keyword%' ";
} elseif ($field == 'name') {
$where .= " and name like '%$keyword%' ";
}
if ($between_time != '') {
$time_arr = explode('~', $between_time);
$start_time = strtotime($time_arr[0]);
$end_time = strtotime($time_arr[1]);
$where .= " and add_time > $start_time and add_time < $end_time";
}
//是否打印如果是-1,则不过滤
if ($is_print != '-1') {
$where .= " and is_print ='$is_print' ";
}
$arr_data = Db::table('client_order_info')->where($where)->where("pay_status='1'")->order('add_time', $orderby)->limit($offset . ',' . $pagesize)->select();
if (empty($arr_data)) {
throw new \Exception('没有选中数据');
die;
}
$order_ids = '';
$daochu[] = array('订单唯一标识', '收货人姓名', '收货人手机号', '收货人地址', '商品编号', '商品名称', '订单号', '订单状态', '快递名称', '快递单号', '其他运单号', '运费', '商品价格', '订单金额', '下单时间', '客户备注', '客服备注', '支付方式', '在线支付金额', '余额支付金额', '支付时间', '是否vip订单');
foreach ($arr_data as $key => $value) {
$order_ids .= $value['order_id'] . ',';
foreach ($value as $k => $v) {
switch ($k) {
case 'keynum':
$r[0] = $value['keynum'];
break;
case 'consignee':
$r[1] = $value['consignee'];
break;
case 'phone':
$r[2] = $value['phone'];
break;
case 'detail_address':
$r[3] = $value['province'] . $value['city'] . $value['area'] . $value['town'] . $value['detail_address'];
break;
case 'goodssku':
$r[4] = $value['goodssku'];
break;
case 'goodsinfo':
$r[5] = $value['goodsinfo'];
break;
case 'order_sn':
$r[6] = $value['order_sn'];
break;
case 'order_status':
$r[7] = get_order_status($value['order_id']);
break;
case 'shipping_name':
$r[8] = $value['shipping_name'];
break;
case 'shipping_num':
$r[9] = $value['shipping_num'];
break;
case 'shipping_othernum':
$r[10] = $value['shipping_othernum'];
break;
case 'shipping_fee':
$r[11] = $value['shipping_fee'];
break;
case 'goods_total_money':
$r[12] = $value['goods_total_money'];
break;
case 'order_total_money':
$r[13] = $value['order_total_money'];
break;
case 'add_time':
$r[14] = date_format_tpl($value['add_time']);
break;
case 'remark':
$r[15] = $value['remark'];
break;
case 'admin_remark':
$r[16] = $value['admin_remark'];
break;
case 'pay_method':
$r[17] = get_paymethod_names($value['keynum']);;
break;
case 'online_money':
$r[18] = $value['online_money'];
break;
case 'member_money':
$r[19] = $value['member_money'];
break;
case 'pay_time':
$r[20] = date_format_tpl($value['pay_time']);;
break;
case 'is_vip':
$r[21] = get_order_vip_name($value["is_vip"], "excel");
break;
default:
break;
}
}
ksort($r);
$daochu[] = $r;
}
$daochu = array_filter($daochu);
$order_ids = substr($order_ids, 0, strlen($order_ids) - 1);
$date = date('Y-m-d', time());
$xls_name = $date;
$binfo = Db::table('client_order_batch_chu')->where('date', $date)->where('clientkeynum', $basekeynum)->order('id desc')->find();
if ($binfo) {
$pcname_arr = explode('-', $binfo['name']);
$pcnum = $pcname_arr[3] + 1;
$pcname = $date . '-' . $pcnum;
} else {
$pcname = $date . '-1';
}
$batch = [
'clientkeynum' => $clientkeynum,
'order_id' => $order_ids,
'addtime' => time(),
'action_user' => session('cn_accountinfo.accountname'),
'name' => $pcname,
'date' => date('Y-m-d', time()),
];
//没有要下载的数据
if ($order_ids == '') {
throw new \Exception('没有选中数据');
die;
}
foreach ($arr_data as $k => $v) {
//订单操作日志
$log['order_sn'] = $v['order_sn'];
$log['clientkeynum'] = $clientkeynum;
$log['action_user'] = session('cn_accountinfo.accountname');;
$log['action_note'] = '勾选导出订单';
$log['add_time'] = time();
$log_flag = Db::table('client_order_log')->insert($log);
if (!$log_flag) {
throw new \Exception('insert,client_order_log失败!');
}
}
//把这些订单改成已导出状态
$order_up = Db::table('client_order_info')->where("order_id in ($order_ids)")->update(['is_daochu' => 1, 'daochu_time' => time()]);
if (!$order_up) {
throw new \Exception('update,client_order_info失败!');
}
$insert = Db::table('client_order_batch_chu')->insert($batch);
if (!$insert) {
throw new \Exception('insert,client_order_batch_chu失败!');
}
// 提交事务
Db::commit();
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
$msg = $e->getMessage();
$trans_result = false;
} finally {
//无论是否正常或抛出异常,finally 块中的代码都会执行,注意try里面要尽量包含查询,然后就是里面不要有die,一定要抛出异常否则锁不能自动删除。
// 释放锁
$deleted = $lock->release();
}
if (!$trans_result) {
error_tips($msg);
die;
}
addoperatelog('批量导出订单', json_encode($param, JSON_UNESCAPED_UNICODE));
create_xls($daochu, $pcname);
}
4.下面这种thinkphp5封装的不可行
//Cache类的set方法并不直接支持Redis的SET命令的所有选项,如NX和EX。因此,我们需要使用更底层的Redis客户端来实现这一点。
public function redis_lock1()
{
// redis锁,避免短时间重复点击导出
$lock_key = 'dao_chu_order_son_lock1';
$lock_ttl = 10; // 锁的超时时间(秒)
// 记录开始时间
$start_time = microtime(true);
$lock_info = Cache::store('redis')->get($lock_key);
var_dump($lock_info);
// 尝试获取锁,不支持NX和EX,每次都是返回true,所以没得办法,只能用原生的redis客户端
$acquired = Cache::store('redis')->set($lock_key, 1, ['nx', 'ex' => $lock_ttl]);
var_dump($acquired);
if (!$acquired) {
error_tips('导出操作正在进行中,请稍后再试!');
die;
}
try {
echo "我是业务内容";
sleep(4);
} finally {
// 记录结束时间
$end_time = microtime(true);
$execution_time = $end_time - $start_time;
// 释放锁
$deleted = Cache::store('redis')->rm($lock_key);
var_dump($deleted); // 检查锁是否被成功删除
// 输出执行时间
echo "执行时间: " . $execution_time . " 秒";
}
}
//Cache类的set方法并不直接支持Redis的SET命令的所有选项,如NX和EX。因此,我们需要使用更底层的Redis客户端来实现这一点。
public function redis_lock11()
{
// redis锁,避免短时间重复点击导出
$lock_key = 'dao_chu_order_son_lock1';
$lock_ttl = 10; // 锁的超时时间(秒)
// 记录开始时间
$start_time = microtime(true);
$lock_info = Cache::store('redis')->get($lock_key);
var_dump($lock_info);
// 尝试获取锁,不支持NX和EX,每次都是返回true,所以没得办法,只能用原生的redis客户端
$acquired = Cache::store('redis')->set($lock_key, 1, ['nx', 'ex' => $lock_ttl]);
var_dump($acquired);
if (!$acquired) {
error_tips('导出操作正在进行中,请稍后再试!');
die;
}
try {
echo "我是业务内容";
sleep(4);
} finally {
// 记录结束时间
$end_time = microtime(true);
$execution_time = $end_time - $start_time;
// 释放锁
$deleted = Cache::store('redis')->rm($lock_key);
var_dump($deleted); // 检查锁是否被成功删除
// 输出执行时间
echo "执行时间: " . $execution_time . " 秒";
}
}
版权声明:若无特殊注明,本文皆为《菜鸟站长》原创,转载请保留文章出处。
本文链接:thinkphp5中基于redis的分布式锁实现方案 - http://www.wlphp.com/?post=448