一、锁的背景
锁在我们的日常开发可谓是高并发的代言词,通常用来解决资源并发的问题。特别是多机集群情况下,资源争抢的问题。但是,很多新手在锁的处理上常常会犯一些问题。今天我们来深入理解锁。
二、基于数据库实现分布式锁
1. 悲观锁
利用select … where … for update 排他锁
注意: 其他附加功能与实现一基本一致,这里需要注意的是“where name=lock ”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。
2. 乐观锁
所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。
通过增加递增的版本号字段实现乐观锁
![350cad9be883d6fe464e2fcfea2fdc7e.png](https://i-blog.csdnimg.cn/blog_migrate/3ef8ae6313aea96488774ed736313f17.jpeg)
3.database实现分布式锁原理
数据库实现分布式锁的方式和redis分布式锁的实现方式类似,这里采用数据库表的唯一键的形式。如果同一个时刻,多个线程同时向一个表中插入同样的记录,由于唯一键的原因,只能有一个线程插入成功。流程图如下:
![27a563b16b9c3bb267d0cc2c6e2ecafd.png](https://i-blog.csdnimg.cn/blog_migrate/8a1aa5e4e22e6bca09502a28498915b9.jpeg)
三、基于缓存(Redis等)实现分布式锁
1. 使用命令介绍:
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete
delete key:删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。
2. 实现思想:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
(4)接入lua脚本(保证脚本中的命令被一起执行,不间断)来实现分布式锁。同时有嗯可以把以上操作,获取锁,释放锁都可以放在lua脚本去中去执行,来保证原子性。如下:
![0113cb40bc28f4e99fee4773219d0d0d.png](https://i-blog.csdnimg.cn/blog_migrate/ffbc4f0717d801ed9875e3576af4169f.jpeg)
四、Redis 锁错误使用之一
我曾经见过有的项目把查询结果存储到 Redis 当中时的伪代码如下:
$redis = new Redis('127.0.0.1', 6379);
$cacheKey = 'query_cache';
$result = $redis->get($cacheKey);
if ($result) { // 缓存有效则直接返回。
return $result;
} else { // 缓存失效则重新获取并存储到 Redis。
$mysqlResult = [];
$redis->set($cacheKey, json_encode($mysqlResult), 3600);
return $mysqlResult;
}
初看代码并不会发现问题所在。通常情况下,当服务器资源压力非常小的时候,这段代码不会有任何问题。并且,真的可以提升服务器吞吐性能。
假如,这个位置的代码出现了单点压力呢?比如,这个功能是统计结果,查询数据库需要花 5s。而且,由于该功能比较常用,单位时间内达到了 1000 次/秒。
这时就会出现并发穿透问题。
1000 个请求同时到达这个程序位置,都去读取缓存是否存在。假如此时缓存不存在。这 1000 个请求都会得到不存在的结果。并且都会执行到去数据库取缓存结果的步骤。同时也会把结果重写到 Redis。
那就导致了这一瞬间单点压力导致穿透到数据库,造成数据库压力瞬间到达峰值。如果我们的数据库的性能处理不了这么大的压力,就会导致数据库服务器 CPU 直接爆满。响应给前端的数据就会陷入停顿状态。
所以,这段代码是不正确的锁使用。
五、Redis 锁错误使用之二
在第一点中,我们发现了问题。于是,就有人想着去优化它。于是就有了下面的代码:
$redis = new Redis('127.0.0.1', 6379);
$lockKey = 'query_cache_lock'; // 锁专用的 KEY。
$cacheKey = 'query_cache'; // 存储查询结果的 KEY。
$result = $redis->get($cacheKey);
if ($result) { // 缓存有效则直接返回。
return $result;
} else { // 缓存失效则重新获取并存储到 Redis。
if ($redis->setNx($lockKey) === false) {
throw new Exception("服务器火爆,请稍候重试");
} else {
$mysqlResult = [];
$redis->set($cacheKey, json_encode($mysqlResult), 3600);
$redis->delete($lockKey); // 锁用完了要解锁。删掉就是解锁。
return $mysqlResult;
}
}
这段代码就完全避免了第一点中的并发穿透的问题。但是,相对第一点,代码也多增加了几行。不过性能依然强劲。
即使如此,这段代码依然存在三个问题:
1)并发越大,第一个取到锁的请求能正常响应,后续的请求就会得到一个“服务器火爆,请稍候重试”的异常提示。
2)没办法对后续请求取锁失效加一个等待时间。
3)如果代码执行到 $redis->delete($lockKey) 之前程序异常了。那么锁就不能正常释放。后续的锁也无法正常取到锁了。
针对第 1) 点,这个是用户体验极差的。
针对第 2) 点,它是解决第一点的方案。
针对第 3) 点,它是我们必须解决的问题。否则,我们的分布式锁将无法正常使用。
六、正确的分页式锁
正常的分布式锁要满足以下几点要求:
1)能解决并发时资源争抢。这是最核心的需求。
2)锁能正常添加与释放。不能出现死锁。
3)锁能实现等待,否则不能最大保证用户的体验。
针对以上三点,得出 Redis 分布式锁示例
class RedisMutexLock
{
/**
* 缓存 Redis 连接。
*
* @return void
*/
public static function getRedis()
{
// 这行代码请根据自己项目替换为自己的获取 Redis 连接。
return YCache::getRedisClient();
}
/**
* 获得锁,如果锁被占用,阻塞,直到获得锁或者超时。
* -- 1、如果 $timeout 参数为 0,则立即返回锁。
* -- 2、建议 timeout 设置为 0,避免 redis 因为阻塞导致性能下降。请根据实际需求进行设置。
*
* @param string $key 缓存KEY。
* @param int $timeout 取锁超时时间。单位(秒)。等于0,如果当前锁被占用,则立即返回失败。如果大于0,则反复尝试获取锁直到达到该超时时间。
* @param int $lockSecond 锁定时间。单位(秒)。
* @param int $sleep 取锁间隔时间。单位(微秒)。当锁为占用状态时。每隔多久尝试去取锁。默认 0.1 秒一次取锁。
* @return bool 成功:true、失败:false
*/
public static function lock($key, $timeout = 0, $lockSecond = 20, $sleep = 100000)
{
if (strlen($key) === 0) {
// 项目抛异常方法
YCore::exception(500, '缓存KEY没有设置');
}
$start = self::getMicroTime();
$redis = self::getRedis();
do {
// [1] 锁的 KEY 不存在时设置其值并把过期时间设置为指定的时间。锁的值并不重要。重要的是利用 Redis 的特性。
$acquired = $redis->set("Lock:{$key}", 1, ['NX', 'EX' => $lockSecond]);
if ($acquired) {
break;
}
if ($timeout === 0) {
break;
}
usleep($sleep);
} while (!is_numeric($timeout) || (self::getMicroTime()) < ($start + ($timeout * 1000000)));
return $acquired ? true : false;
}
/**
* 释放锁
*
* @param mixed $key 被加锁的KEY。
* @return void
*/
public static function release($key)
{
if (strlen($key) === 0) {
// 项目抛异常方法
YCore::exception(500, '缓存KEY没有设置');
}
$redis = self::getRedis();
$redis->del("Lock:{$key}");
}
/**
* 获取当前微秒。
*
* @return bigint
*/
protected static function getMicroTime()
{
return bcmul(microtime(true), 1000000);
}
}
以上是其中一些项目有用到的分布式锁,大家也可以更换为自己项目
如果你喜欢我写的技术文章以及面试总结,也欢迎关注收看我的视频,并且点赞、收场、关注 我哦。
![5a829f2e1a183182545cb1e1529c9e24.png](https://i-blog.csdnimg.cn/blog_migrate/b48b133e96d1d6965baae8be4d5c3128.jpeg)
以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要的可以加入我的PHP技术交流群:1071033649