68. 锁lock服务
我们知道web服务器是并发访问的,php脚本被不同的线程或进程执行,他们可能真正的同时执行,现在假设有个操作(比如计划任务)在系统中只能被执行一次,由于这种并发性就可能出现问题,两个同时执行的请求可能带来两次执行,为了解决这个问题我们就需要“锁LOCK”了。
锁LOCK服务定义:
锁是系统定义的一个服务,定义如下:
lock:
class: Drupal\Core\Lock\DatabaseLockBackend
arguments: ['@database']
tags:
- { name: backend_overridable }
lazy: true
运行时该服务是通过代理类执行的,导出容器定义数据可以看到服务id:“lock”对应的是代理类:
Drupal\Core\ProxyClass\Lock\DatabaseLockBackend
在容器中,她真正的服务名是:
drupal.proxy_original_service.lock
代理类可以让服务只有在真正使用时才被实例化,而不是获取时立即实例化。
获取锁服务的方法:$lock=\Drupal::lock()
此处$lock称为锁对象,由她来建立和管理锁,从容器中只能获取一个锁对象,当某些高级操作需要多个锁对象时可以直接实例化:
$lock_1=new \Drupal\Core\Lock\DatabaseLockBackend($database);
但需要多个锁对象的情况非常罕见,系统中也没有用到的案例,一般不需要。
锁原理:
锁的作用是进行排他操作,保证在同一时间段里只有得到锁的请求才能进行被锁定的操作,可保证在同一时间段里的唯一性,避免并发问题,不被其他请求打扰。
drupal锁系统在数据库中设置一个数据表来存放锁信号,表名为:“semaphore”,有三个字段:
name:
锁名称,代表着某一个操作,由开发者决定,往往使用函数名,数据表主键,不可重复
value:
锁id,一个有唯一性的随机字符串,代表着一个锁对象,相同锁对象设置管理的所有锁具备相同锁id,往往一个请求只有一个锁对象,所以有时可以近似的讲一个请求中设置的所有锁使用相同id,通过她可以用来释放一个请求中的所有锁,但当一个请求有多个锁对象时应该知道她代表锁对象而非请求
expire:
unix时间戳,浮点值,代表锁的到期时间,超期后锁将失效(又称为锁被释放)
当两个或多个同时执行的并发请求欲执行一个只能执行一次的操作时,她们应该获取一个锁,只有得到锁后才能开始操作,竞争锁的过程就是看谁先在锁信号表中写入数据的过程,一旦其中一个请求在该表中写入了信息,就意味着她得到了锁,由于锁名name是数据库表的主键,主键不可以重复,一旦有写入,那么其他请求将不能再进行写入,也就不能得到锁,从而保证了在并发中只有一个请求能得到锁,从而被锁定的操作只能执行一次,本质上锁是由数据库来进行保障的,当锁定的操作执行完成应该明确释放锁,也就是从数据库中删除锁信息。
用法示例:
$operationID = "yunke";
//代表着将要进行的操作的标识符,往往可以设置为函数名,就是数据库的name值
$lock = \Drupal::lock(); //得到锁对象
if ($lock->acquire($operationID)) {
// 只有得到了锁才能执行操作,否则应该继续等待或放弃执行
sleep(30); //在本代码运行期间暂停30秒,30内你可以看到数据库被写入了锁信息
$lock->release($operationID);
// 当操作结束后应当明确释放锁,依赖锁自动过期有性能损耗
}
锁对象方法详解:
在接口:
\Drupal\Core\Lock\LockBackendInterface
中有说明,这里做进一步的解释:
public function acquire($name, $timeout = 30.0)
申请一个锁,如果申请到了将返回true,否则false,参数$name为要使用的锁名,应该是代表某种操作的标识符,又开发者决定,不强制,往往是函数名,锁名为ASCII编码的字符串,小于等于255个字符,否则内部将使用Base64进行编码,$timeout指定锁的有效时间,一旦超过将失效,默认为30秒,开发者应该估算合适的时间值,在申请成功后还可以再次调用该方法,表示将锁延期,新的锁有效期是从当前开始加$timeout指定的时间,而不是在之前的到期时间基础上加$timeout,由于这点当之前指定的有效期太长时可以再次调用指定一个短的有效期以尽快释放锁,如果再次调用时返回false,那么表示锁已经被其他请求占用,应该放弃当前工作或继续等待。
public function lockMayBeAvailable($name)
判断锁是否可以进行申请,比如虽然锁被另外的请求申请到了,期效已过但却没有被释放,那么该方法将强制释放,并返回true表示锁可申请,返回true时表示此时可能成功申请到锁,返回false时表示锁还在占用中
public function release($name)
根据锁名释放锁,当锁用完时,应该释放她以便其他请求使用,如果锁超期,即便没有明确释放,在其他请求申请时系统也会收回她
public function releaseAll($lock_id = NULL)
通过锁id释放锁,这将释放一个锁对象管理的所有锁,该方法被构造函数注册为关机函数,在脚本结束时自动执行,参数$lock_id默认为null,此时将释放本锁对象管理的所有锁,云客认为该方法需要改进,开发者可能本意想通过锁id能够释放其他锁对象管理的锁,但在该方法内却又仅在本对象有分配锁时才进行释放操作,好在通常一个请求只有一个锁对象,无需这样的操作;脚本结束时默认会释放本锁对象管理的所有锁
public function getLockId()
得到锁id,在锁服务(不是锁,而是锁对象, $lock = \Drupal::lock();)生存周期内id不变,运用于所有的锁,是一个随机字符串,每个锁对象都不一样,在同一个请求中如果初始化多个锁对象,那么该请求得到的锁将有多个id,删除时需要用releaseAll方法,或者等待超期自动删除。
public function wait($name, $delay = 30)
当申请锁失败时,可以调用该方法以在$delay指定的时间内等待,最多等待$delay指定的时间,单位秒,默认30秒,等待期间一旦发现锁有可能申请成功,那么她立即返回false,这表示不用再等,可马上申请,但不保证一定能申请成功,如果返回true则表示依然不能申请到锁,需要继续等待,在内部使用php函数usleep间隙性暂停代码执行以等待申请时机,示例如下:
$lock = \Drupal::lock();
$operationID = "yunke";
$is_get_lock = false;
$is_get_lock = $lock->acquire($operationID);
if (!$is_get_lock) {
if (!$lock->wait($operationID, 5)) {
$is_get_lock = $lock->acquire($operationID);
}
}
if ($is_get_lock) {
//do something
$lock->release($operationID);
}
持久锁:
系统还定义了一个持久锁,用于系统安装过程中,她的锁id是固定的且没有注册关机函数,其他和锁服务一样,不做详解,定义如下:
lock.persistent:
class: Drupal\Core\Lock\PersistentDatabaseLockBackend
arguments: ['@database']
tags:
- { name: backend_overridable }
lazy: true
补充注意:
- 锁系统采用的是脚本运行所在主机上的时钟,在管理员改变服务器时间或者通过网络时间协议NTP同步时间时将对锁系统产生影响
- 如果网站有多台主机,每个主机时间可能有差异,这将可能导致问题。
- 在进行被锁定资源的共享时,也必须共享存放锁的信息表“semaphore”,对于系统之外的操作,锁系统并不能阻止