# 什么是锁?
如果从日常生活中理解什么是锁,很好理解,每个人家门上都有锁,用来防止他人窃取自家财产。但是在计算机中,锁的概念稍有不同,在计算机中只有涉及到资源竞争的时候,才会用到锁。
比如在单线程中,不需要用到锁,资源都是顺序化被持有,不存在竞争。但是在多线程中,同时会有多个请求需要同一个资源,这个时候,就需要进行加锁操作,一个线程获取到锁之后,其他的线程只有等待资源被释放才能接着执行。
# 锁的作用
锁本质是为了保证串行,比如在购买订单的时候,同时涌入大量的请求,如何保证商品不多卖以及不少卖,保证数据的准确性,这个时候就要用锁来控制并发,让本来并行执行的问题转换为串行执行。
# 锁的分类
说到锁的分类,在各种文章以及研究中都提供了不同的分类,比较繁杂,但是如果从思想上来说,总体来说分为两类,一类是悲观锁,一类是乐观锁。
# 乐观锁
乐观锁是相对于悲观锁而言,乐观锁机制采用了更加宽松的加锁机制。乐观锁字如其名,就是持有比较乐观的态度。就是假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则给用户返回错误信息,让用户决定如何去做。
乐观锁的实现也比较简单,就是使用数据版本version记录机制实现。接下来,我们利用mysql数据库来实现一下乐观锁,但是这里要特别注意,第一点,mysql本身并没有提供实现乐观锁,而且也没有乐观锁这个概念,mysql的锁都是悲观锁,那么我们就懂了,乐观锁是一种思想,使用其他的所有方式都可以实现,只要实现了这个思想的,都叫乐观锁。比如可以使用文件,使用redis都可以实现。
我们首先建立一张表叫good\_num货物表。
创建表的语句如下所示
~~~
CREATE TABLE `good_nums` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`nums` int(11) DEFAULT NULL,
`version` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
~~~
| 字段 | 描述 |
| --- | --- |
| id | 自增ID |
| name | 名字 |
| nums | 数量 |
| version | 版本 |
接下来我们给表增加一条记录。
~~~
INSERT INTO good_amount (name,nums,version) VALUES('张三',100,1)
~~~
目前记录如下所示:
| id | name | nums | version |
| --- | --- | --- | --- |
| 1 | 张三 | 100 | 1 |
OK,基本情况我们已经构建完成。
那么现在我们假定有这么一个需求,用户每买一笔,就要把数量减少1个。理想情况下,如果用户一个一个来去购买的话,我们的数量会按照情况去一个一个减少。但是这仅仅是理想情况,实际情况是,用户会同时涌入来购买,在这种情况下,数量的减少根本无法保障。
我们看一下在未使用锁的情况下,是如何实现程序的,这也是初级程序员最常写的代码如下所示:
~~~
<?php
//连接数据库
$conn = new mysqli_connect(xxxxx);
//查询当前的数量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遗憾,商品被抢光了";
exit;
}
//将当前的数量减少1
$nums = $row['nums'] - 1;
$sql1 = "update good_nums set nums=".$nums." where id=1";
$source1 = $conn->query($sql1);
~~~
通过上面的代码可以很明显的理解,当大量的请求同时涌来的时候,程序在同一时间可能同时读到当前数量为100,那么每个用户可能都抢到了物品,但是数据库记录数到最后居然是99,然而此时可能已经卖了1千个了。此时就发生了超卖的问题。
那么如果此时使用乐观锁,就很容解决这个问题了。代码如下所示:
~~~
// 连接数据库
$conn = new mysqli(xxxx);
//查询当前的数量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遗憾,商品被抢光了";
exit;
}
//给当前的数量减少1
$nums = $row['nums'] - 1;
//当前的版本
$version = $row['version'];
//更新数据库的数量
$sql1 = "update good_nums set nums=".$nums.",version=version+1 where id=1 and version=".$version;
$source1 = $conn->query($sql1);
if(!$source1){
echo "您未抢到本商品,请继续努力";
}
$conn->close();
~~~
可以对比现在的程序,比之前的程序多增加了一个version的条件控制,对的,这就是乐观锁的精髓所在。
我们现在来模拟一下用户的请求。
当第一个用户涌进来,他拿到的货物数量为100,此时拿到的version为1。
当第二个用户涌进来,他此时拿到的货物数量也为100,此时的version为1。
还有第三个,第四个,都是同样的情况。
此时,第一个用户的请求去更新数据库,他更新的条件,是version必须等于刚刚拿到1,此时数据库的version还未更新,于是他将这条记录更新成功,nums数量成功减少为99,并且于此同时,将version更新为2。
那么第二个用户也去执行这个条件,数据库此时去检查发现version居然不是1了,于是这个条件不成立,此时这条sql就会执行失败,然后告诉用户你没有抢到。
一直第三个,第四个用户都是如此。
只有第n个用户可能拿到新的version,并且可能成功更新。
上面就是乐观锁的完整实现,当前了,上面我们写的程序还有问题,下面是一个正确的程序。
~~~
// 连接数据库
$conn = new mysqli(xxxx);
//查询当前的数量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遗憾,商品被抢光了";
exit;
}
//当前的版本
$version = $row['version'];
//更新数据库的数量
$sql1 = "update good_nums set nums=nums-1,version=version+1 where id=1 and version=".$version;
$source1 = $conn->query($sql1);
if(!$source1){
echo "您未抢到本商品,请继续努力";
}
$conn->close();
~~~
仔细对比就会发现,之前我们是用程序去减少的数量1,但是现在的代码是让数据库去自动减少1。这个也是比较关键的一点,在高并发的情况下,请记住一定要如此写,不然可能会发生不可想像的错误。
在很多时候,如果面试官继续深究,他就会问你,如果大量的用户同时涌入,上面的程序只能保证少数人能拿到商品啊,可能100个用户同时涌入,到最后,只卖了10个,但是公司又想这100都卖掉,这个时候咋办?
这个时候我们就引入了自旋锁的概念。这个概念如果第一次听说,就会有点蒙,啥是自旋,怎么自旋,好,带着这个问题,我们回到刚刚100个人来抢,可能只卖了10个问题。
我们想,既然用户会发生更新失败的问题,我们为啥不如让用户等待一下,重新获取一遍新的值,然后让用户抢到呢?对的,这就是自旋锁了。
程序如下:
~~~
// 连接数据库
$conn = new mysqli(xxxx);
$tips = true;
while($tips){
//查询当前的数量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遗憾,商品被抢光了";
$tips = false;
exit;
}
//当前的版本
$version = $row['version'];
//更新数据库的数量
$sql1 = "update good_nums set nums=nums-1,version=version+1 where id=1 and version=".$version;
$source1 = $conn->query($sql1);
//当数量更新失败了
if($source1 == true){
$tips = fasle;
}
}
echo "恭喜您抢到了商品";
$conn->close();
~~~
我们看到,首先我们会给一个标识位为true,就是只有不满足条件的时候才退出,也就是只有用户抢到了,才结束掉本次请求。如果没有抢到,程序会不断的请求,让当前的用户去抢,这就是自旋锁,实际上,就是用了一个循环,不端的去请求,当然了,也可以用递归,但是本质都是相同的,都是循环。
但是上面的程序有个问题,如果用户一直没有抢到,程序就会一直执行,如果请求数量巨大,就会发生大量的timeout,我们能不能像个办法,提前结束掉循环呢?程序如下:
~~~
// 连接数据库
$conn = new mysqli(xxxx);
$tips = true;
$count = 5;
while($tips){
//查询当前的数量
$sql = "SELECT * from good_nums where id =1";
$source = $conn->query($sql);
$row = $source->fetch_assoc();
if($row['nums'] <= 0){
echo "很遗憾,商品被抢光了";
$tips = false;
exit;
}
//当前的版本
$version = $row['version'];
//更新数据库的数量
$sql1 = "update good_nums set nums=nums-1,version=version+1 where id=1 and version=".$version;
$source1 = $conn->query($sql1);
//当数量更新失败了
if($source1 == true){
$tips = fasle;
}
$count--;
if($count <= 0){
$tips = false;
}
}
echo "恭喜您抢到了商品";
$conn->close();
~~~
上面的程序,我们给程序增加了一个count值,当程序执行超过了限定,我们就会释放掉本次循环,需要说明的是,这不是自旋锁的概念,只是为了优化请求,才这么写的,为的是保证程序不超时。
自旋锁如果按照简拼来说,通常被叫做CAS。面试的时候,如果被问题到CAS是什么,一定要知道,指代的是自旋锁。
但是上面的自旋锁还是有缺点的,就是在未加count的控制的时候,程序会不断地循环,会给CPU造成多余的计算能力,为了解决这个问题,对于自旋锁又提出了其他的实现方法,如果有兴趣可以自行百度。一般来说,自旋锁的概念多在Java
面试中提及,因为Java本身支持锁操作,也支持多线程,可以利用多线程对自旋锁进行优化,但是本质都是一样的,即循环抢占锁。
# 悲观锁
悲观锁,顾名思义,就是很悲观,在每次操作的时候,都认为别人已经进行了修改,所以,每次去拿数据的时候都会先进行加锁操作,防止其他人抢占资源。
接下来,将使用给文件加锁的形式实现悲观锁。代码如下:
~~~
<?php
$file = "/home/work/abc.txt";
//给文件加锁
if(flock($file,LOCK_EX|LOCK_NB)){
//这里表示抢占到了锁,可以执行业务逻辑了
//todo .....
//执行完成之后,记得要释放掉锁
flock($file, LOCK_UN);
}else{
echo "很不好意思,您没有抢到锁";
}
~~~
通过上面的代码,我们就很容易理解悲观锁,当请求过来的时候,我们先将文件锁定,抢占到这个资源,如果于此同时再有其他到请求过来,他们没有抢占到锁,其他的任务都会失败。只有等待抢占锁的任务成功释放掉锁之后,其他的任务才可以继续抢占锁从而继续任务。
当然了,我这里写的代码是直接告诉用户
# 乐观锁和悲观锁的对比
通过上面乐观锁和悲观锁的例子,我们就很容易理解了。
乐观锁保持乐观的态度,不会一进来就认为别人动了自己的资源,只有更新的时候,才会检查一下,如果此时发现被别人更改过了,那么就直接返回失败,乐观锁适用于读多写少的场景,我们想,如果一个任务需要大量的更新,使用了乐观锁,那么大部分任务不都将失败了吗?所以要考虑好场景再使用。
悲观锁是保持悲观的态度,一进来就先将资源占有起来,只有自己的任务全部完成之后,才释放资源。
当然了,最后一点,最重要的是理解悲观锁和乐观锁是一种思想,也就是不局限于任何形式的实现。
原文:https://www.kancloud.cn/missyou/interview/2234864
- 后端
- PHP
- php接收base64格式的图片
- php 下载文件
- 位,字节,字符的区别
- 求模技巧
- php curl
- php 浏览器禁用cookie后需要使用session 就可以用url传递session_id
- 有用小方法
- phpDoc
- php 文件锁来解决高并发
- php小知识
- PHP根据身份证号码,获取性别、获取生日、计算年龄等多个信息
- php 获取今天,明天、本周、本周末、本月的起始时间戳和结束时间戳的方法
- php 无限级分类
- xdebug设置
- curl
- 获取现在距离当天结束的还有多少秒
- win10安装php8版本报错(Fix PHP Warning: vcruntime140.dll 14.0 is not compatible with this PHP build.)
- 有趣代码注释
- php array_diff用法
- parse_str 处理http的query参数
- PHP文件上传限制
- php操作html
- php trim 函数的使用
- thinkphp5
- 定时任务不能连接数据库
- 宝塔设置计划任务
- 控制方法 return $data ,不能直接返回json
- tp5.1命令行
- tp3.2.3 报internal server error
- 悟空crm
- web-msg-sender的使用
- 杉德支付
- laravel
- laravel 迁移文件的使用
- laravel的安装
- laravel 单元测试
- laravel seeder的使用
- 模型相关
- restful理解
- laravel 的表单验证
- laravel 队列的使用
- laravel响应宏应用macro
- laravel 判断集合是否为空
- laravel 使用ymondesigns/jwt-auth jwt
- laravel 模型工厂
- laravel 自定义助手函数
- laravel 自带auth的登录
- 宝塔开启laravel队列
- laravel 苹果内购
- laravel 中的.env.example
- laravel 监听执行过的sql语句
- laravel-websockets 替代pusher 发送频道消息
- 记laravel config配置文件目录中不能使用 url()助手函数
- laravel使用 inspector 进行实时监控
- laravel 项目部署的配置
- laravel 删除mongodb集合
- laravel 自定义项目命名空间
- laravel 易错提醒
- laravel 自己组装分页
- laravel 设置定时任务
- laravel事件和队列指定队列名
- laravel 使用validate检测名字是否唯一
- laravel + nginx 伪静态分析
- fastadmin
- cms
- 标签
- 模板
- dact-admin
- dcat-admin的安装
- dcat-admin的curd使用
- dact-admin表单使用
- dcat-admin行为表单使用
- dcat-admin使用技巧
- dcat-admin自定义文件上传
- dcat-admin的js弹窗
- dcat-admin 工具表单传参
- dcat-admin listbox编辑回显用法
- weixin
- 微信支付
- 支付类
- 小程序
- 微信提现类
- jwt
- lcobucci/jwt
- Firebase\JWT
- phpstudy
- nginx配置tp5 505 404 错误
- tp5重写 apache
- 织梦模板 使用weight 排序
- phpstudy 添加php8.1版本
- phpstudy ERR_CONNECTION_REFUSED
- phpstudy 设置多个版本php
- 阿里云
- 支付宝支付
- 阿里云短信
- 阿里云OSS上传图片报错
- 阿里云号码认证(一键登录)
- send login code error: 发送验证码失败:cURL error 28: Connection timed out after 5001 milliseconds
- 极光号码认证(一键登录)
- git使用
- git
- sentry专栏
- sentry的私有化部署
- sentry设置邮箱
- sentry设置url地址
- sentry中KafkaError OFFSET_OUT_OF_RANGE error
- centos
- tar 压缩解压
- centos 8 Errors during downloading metadata for repository 'appstream'
- vim的使用
- ssh秘钥登录
- 修改了.bashrc不能立即生效
- 设置软连接
- 使用echo清空文件内容
- 查看文件大小
- centos8 设置静态ip
- nginx
- nginx的学习
- nginx配置wss
- supervisor的使用
- shell的使用
- 数据库
- mysql
- mysql的事务隔离级别
- mysql共享锁和排它锁
- mysql的三范式
- mysql 在那些场景下索引会失效
- mysql 的书写顺序
- mysql case用法
- mysql 以逗号分割字符串
- msyql innodb 行锁解决高并发
- mysql修改字符集
- 锁
- 乐观锁悲观锁
- mysql 最左索引原则
- mysql 同表两列值互换
- mysql升序排列字段为0的在最后
- mysql case when then else end 语法
- mysql 常见错误
- mysql json用法
- MongoDB
- mongodb安装
- redis
- redis 常用通用命令
- string类型的常见命令
- 连接远程redis删除指定的值
- markdown
- markdown的使用
- github
- github使用小技巧
- jenkins
- 安装jenkins
- jenkins设置时区
- docker
- 安装docker
- docker容器设为自启动和取消容器自启动
- docker 安装mysql
- docker-compose
- docker 安装php
- docker-compose安装nginx
- docker-compose安装php
- docker安装php+supervisor
- composer使用
- composer
- win10检查端口占用
- 局域网内同事访问自己的项目
- 本地测试设置https办法
- 正则表达式
- 前端代码和后台代码部署在一起的解决方法
- pc微信抓包小程序
- xshell一年后提示需要更新才能打开
- 使用ssh秘钥登录服务器
- supervisor
- supervisor的使用
- 浏览器的强制缓存和协商缓存
- window11下ssh远程登录服务器
- chatgpt
- 注册chatgpt
- 第三方chatgpt地址
- 前端
- jquery 常用方法
- jquery 省市区三级联动
- 百度地图短地址
- npm
- webpack
- vue
- 谷歌安装vue-devtools的使用
- swiper 一屏显示页面
- 腾讯地图
- jquery点击图片放大
- 移动端rem适配
- 弹性布局flex
- CSS
- box-sizing
- 移动端去掉滚动条
- 三角形
- 树形结构
- require.js的使用
- 微擎人人商城
- 人人商城弹出框
- 常用方法
- 客服消息
- 企业支付到零钱
- 修复权限问题
- 获取access_token
- 其他管理员没有应用 调用不了p方法
- 修改公众号推送消息
- 人人商城
- 人人商城二开常见问题
- 人人商城应用显示隐藏
- 微擎
- 人人商城小程序解密登录
- 面试题
- 遍历目录中的文件和目录
- 冒泡排序
- php 在字符串中找到最长对称字符串
- 地图相关
- 百度地图根据ip获取地址
- 百度,腾讯,高德,地图点击跳转
- 百度地图根据地址获取经纬度
- 百度地图和腾讯地图经纬度互转
- 其他
- B站跳过充电环节
- 可爱猫咪回收站制作(附图)
- 程序员变量命名网站
- 解决谷歌浏览器强制跳转https
- 随机密码生成
- 编辑器
- vs code使用
- phpstrom
- phpstrom 常用命令
- phpstrom ctrl+b后想回到之前的位置
- phpstrom 批量操作下划线转驼峰
- phpstrom 插件
- phpstrom 使用ctrl+shift+f后搜索不能输入中文
- phpstrom中项目.env文件会自动消失,不显示
- vscode插件