浅谈PHP之线程锁

打印 上一主题 下一主题

主题 825|帖子 825|积分 2475

一、基本先容

PHP 本身是面向 Web 开发的脚本语言,它主要是单线程的。不过在一些特定场景下,比如使用 PHP 举行命令行脚本开发或者在多历程情况下,我们可能必要使用线程锁来保证数据的划一性和完备性。

二、实现方式

一、基于文件的锁



  • 原理

    • 利用文件体系的锁机制来实现。当一个历程必要访问共享资源时,它会尝试对一个特定的文件举行加锁。如果文件已经被其他历程锁定,当前历程就会阻塞等候,直到文件锁被开释。


示例代码
  1. $file = 'lock.txt';
  2. // 获取文件指针
  3. $fp = fopen($file, 'w');
  4. // 加锁
  5. if (flock($fp, LOCK_EX)) { // LOCK_EX 表示排他锁,即同一时间只有一个进程能获取到锁
  6.     // 执行需要同步的代码
  7.     echo "执行同步代码块中的内容" . PHP_EOL;
  8.     // 释放锁
  9.     flock($fp, LOCK_UN);
  10. } else {
  11.     echo "无法获取锁" . PHP_EOL;
  12. }
  13. // 关闭文件指针
  14. fclose($fp);
复制代码
在这个例子中,当一个历程执行到 flock($fp, LOCK_EX) 时,它会尝试对 lock.txt 文件加锁。如果文件已经被其他历程锁定,当前历程会阻塞在这里。当文件锁被开释后(通过 flock($fp, LOCK_UN)),当前历程才能继续执行后续代码。

二、基于数据库的锁



  • 原理

    • 通过在数据库中设置锁记录来实现。可以创建一个专门的锁表,当历程必要访问共享资源时,它会尝试在锁表中插入一条记录或者更新某条记录来获取锁。如果插入或更新操作成功,阐明获取到锁;如果失败,阐明锁已经被其他历程持有。


示例代码(以 MySQL 为例)
  1. $mysqli = new mysqli("localhost", "my_user", "my_password", "my_db");
  2. // 尝试获取锁
  3. $result = $mysqli->query("INSERT INTO locks (resource_name) VALUES ('my_resource') ON DUPLICATE KEY UPDATE process_id = LAST_INSERT_ID(process_id)");
  4. $id = $mysqli->insert_id;
  5. if ($id > 0) {
  6.     // 获取到锁,执行同步代码
  7.     echo "获取到锁,执行同步代码块中的内容" . PHP_EOL;
  8.     // 释放锁
  9.     $mysqli->query("DELETE FROM locks WHERE id = $id");
  10. } else {
  11.     echo "无法获取锁" . PHP_EOL;
  12. }
  13. $mysqli->close();
复制代码
这里假设有一个 locks 表,其中 resource_name 字段用于标识必要锁定的资源,process_id 字段用于记录持有锁的历程 ID。通过 INSERT ... ON DUPLICATE KEY UPDATE 语句尝试获取锁,如果插入成功(即 insert_id 大于 0),阐明获取到锁;如果插入失败(即存在重复的 resource_name),则无法获取锁。


三、基于内存共享的锁(如使用 APCu 扩展)



  • 原理

    • APCu(Alternative PHP Cache user cache)是一个 PHP 扩展,它提供了内存缓存功能。可以利用 APCu 的原子操作来实现锁。当历程必要获取锁时,它会尝试使用 apcu_add 函数在 APCu 缓存中添加一个键值对,如果添加成功,阐明获取到锁;如果添加失败(因为键已经存在),阐明锁已经被其他历程持有。


示例代码
  1. // 尝试获取锁
  2. if (apcu_add('my_lock', true)) {
  3.     // 获取到锁,执行同步代码
  4.     echo "获取到锁,执行同步代码块中的内容" . PHP_EOL;
  5.     // 释放锁
  6.     apcu_delete('my_lock');
  7. } else {
  8.     echo "无法获取锁" . PHP_EOL;
  9. }
复制代码

在这个例子中,my_lock 是锁的键名。当一个历程调用 apcu_add('my_lock', true) 时,如果 APCu 缓存中不存在 my_lock 键,它会添加这个键并返回 true,表示获取到锁;如果 my_lock 键已经存在,apcu_add 函数会返回 false,表示无法获取锁。


三、重要事项

一、死锁问题



  • 定义

    • 死锁是指两个或多个历程在执行过程中,因争夺资源而造成的一种僵局,当历程处于这种僵局时,它们既无法继续执行,也无法终止,只能等候。

  • 常见场景及避免方法

    • 嵌套锁:如果在持有某个锁的情况下又尝试获取另一个锁,而另一个锁已经被其他历程持有,且该历程也在等候当前历程持有的锁开释,就会发存亡锁。例如,历程 A 持有锁 1 并尝试获取锁 2,同时历程 B 持有锁 2 并尝试获取锁 1。

      • 避免方法:只管避免嵌套锁。如果确实必要嵌套锁,可以采用锁次序的方式来避免死锁。即规定全部历程获取锁的次序必须划一。比如,规定先获取锁 1,再获取锁 2,全部历程都按照这个次序来操作锁,就可以避免死锁的发生。

    • 多个资源锁:当多个历程同时竞争多个资源的锁时,也可能出现死锁。例如,有资源 A 和资源 B,历程 A 持有资源 A 的锁并请求资源 B 的锁,历程 B 持有资源 B 的锁并请求资源 A 的锁。

      • 避免方法:可以采用资源分级的方法。给每个资源分配一个等级,历程在请求锁时,必须按照资源等级从低到高的次序来获取锁。如许可以避免形成等候环路,从而防止死锁。



二、锁的开释



  • 重要性

    • 锁的开释是避免资源饥饿和死锁的关键步骤。如果一个历程获取了锁后,没有正确开释锁,其他历程将永远无法获取该锁,导致资源无法被有效利用。

  • 留意事项

    • 确保开释锁:在使用锁的代码块中,一定要确保锁能够在全部可能的执行路径上被开释。可以使用 try...finally 语句来保证锁的开释。例如,在基于文件的锁示例中:

  1. $file = 'lock.txt';
  2. $fp = fopen($file, 'w');
  3. if (flock($fp, LOCK_EX)) {
  4.     try {
  5.         // 执行需要同步的代码
  6.     } finally {
  7.         // 释放锁
  8.         flock($fp, LOCK_UN);
  9.     }
  10. }
  11. fclose($fp);
复制代码



  • 如许即使在执行同步代码块时发生异常,锁也能在 finally 代码块中被开释。
  • 避免提前开释锁:在某些情况下,可能会由于逻辑错误导致锁被提前开释。比如,在一个复杂的业务流程中,某个条件分支错误地执行了锁开释操作,而后续代码还必要使用该锁。这可能会导致数据不划一的问题。因此,要仔细检察代码逻辑,确保锁只在合适的时机被开释。

三、锁的粒度



  • 定义

    • 锁的粒度是指锁所控制的资源范围的大小。粒度可以很粗,比如锁定整个文件或数据库表;也可以很细,比如锁定文件中的某一行或数据库表中的某一条记录。

  • 留意事项

    • 粗粒度锁:固然实现起来相对简朴,但可能会导致资源利用率低下。因为当一个历程锁定整个资源时,其他历程即使只必要访问资源的一部分也会被阻塞。例如,锁定整个数据库表举行更新操作,可能会使其他只必要查询表中部分数据的历程等候。

      • 适用场景:当业务逻辑简朴,对性能要求不是特别高,且资源之间的关联性很强时,可以思量使用粗粒度锁。比如,一个小型的脚本只必要对一个配置文件举行读写操作,使用文件锁来锁定整个配置文件是合适的。

    • 细粒度锁:可以进步资源的并发访问能力,但实现起来相对复杂,且可能会增加锁管理的开销。例如,对数据库表中的每一条记录都加锁,可以允许多个历程同时更新差别的记录,但必要更复杂的锁管理机制来协调这些锁。

      • 适用场景:在高并发的场景下,且资源之间的关联性较弱时,细粒度锁是更好的选择。比如,一个大型的电商平台,必要同时处理惩罚多个用户的订单更新操作,对数据库中的订单表使用行级锁可以进步体系的并发处理惩罚能力。



四、锁的超时机制



  • 定义

    • 锁的超时机制是指在尝试获取锁时,如果在指定的时间内无法获取到锁,则放弃获取锁的操作。这可以避免历程无限期地等候锁,从而进步体系的相应性和稳固性。

  • 实现方法及留意事项

    • 基于文件的锁超时示例

  1. $file = 'lock.txt';
  2. $fp = fopen($file, 'w');
  3. // 设置超时时间
  4. $timeout = 5; // 超时时间为 5 秒
  5. $startTime = time();
  6. while (true) {
  7.     if (flock($fp, LOCK_EX | LOCK_NB)) { // LOCK_NB 表示非阻塞方式获取锁
  8.         // 获取到锁,执行同步代码
  9.         break;
  10.     } else {
  11.         // 检查是否超时
  12.         if (time() - $startTime > $timeout) {
  13.             echo "获取锁超时" . PHP_EOL;
  14.             break;
  15.         }
  16.         // 稍作等待后再次尝试
  17.         usleep(100000); // 等待 0.1 秒
  18.     }
  19. }
  20. // 释放锁
  21. flock($fp, LOCK_UN);
  22. fclose($fp);
复制代码


  • 在这个例子中,通过 LOCK_NB 选项使 flock 函数以非阻塞方式获取锁。如果获取锁失败,就在循环中稍作等候后再次尝试,同时检查是否超时。

  • 留意事项:设置合理的超时时间很重要。超时时间过短可能导致历程频繁地尝试获取锁,增加体系开销;超时时间过长又可能使历程等候过久,影响体系的相应性。必要根据详细业务场景和体系性能来调整超时时间。








免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

飞不高

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表