云服务器 99 / 年,新老同享(可以99/年续费),开发者力荐特惠渠道,新客户在享受9折
阿里云推广

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注