为什么说乐观锁是安全的

最后更新于:2022-04-02 04:29:07

## 为什么说乐观锁是安全的 来看一个具体案例: ```php /** * 检测用户VIP状态,并且当VIP过期时更新VIP状态 * 使用乐观锁,确保仅在不持有旧数据才可以进行操作,因此是安全的 * 返回1说明当前调用的地方持有的已经是旧数据了,要注意乐观锁安全是指它本身,而不是其他地方也是一样,其他地方还是需要自己去控制的 * 2017-6-30 09:32:51 * @param int $uid * @return int 此操作锁影响的行数 */ function checkVip($uid) { $request = Request::instance(); // 这里必须使用乐观锁控制,不然并发会出现严重BUG,这样的话支付通知那里也需要使用乐观锁和行锁啊,不然还是会出现BUG的(事物外的修改对事物内是不可见的) // 这里如果开启事物,可能又会产生事物嵌套的问题,总之要看你能不能承受并发带来的后果了(需要仔细分析一下各种并发情况下有可能会产生的各种问题) // 通过仔细分析发现使用乐观锁,不管什么情况下和支付通知锁那里都是不会产出任何问题的(如果不用乐观锁,那问题就大了) $userInfo = Db::name('user')->find($uid); if (($userInfo['vip_status'] = 2) && ($userInfo['vip_expires_in'] < $request->time())) { $data = [ 'vip_status' => 1, 'update_time' => $request->time(), ]; // ……并发时这中间可能有很多其他的过程,就会出现是有旧数据 // 而乐观锁在更新是的更新条件就是检测数据是不是旧了,所以确保了如果持有旧数据时就什么都做不了,也就安全了 // 返回操作影响的行数,如果有成功的操作,那么会返回1,也说明了没有持有旧数据哦 return Db::name('user')->where(['vip_status' => $userInfo['vip_status'], 'vip_expires_in' => $userInfo['vip_expires_in'], 'update_time' => $userInfo['update_time']])->update($data); } // 没过期什么都不做,返回0 return 0; } // use: /** * 判断是否登录 * @return int 返回uid,没有登录返回0 */ function isLogin() { // (int) 很重要,否则很严重,将会出现重大BUG,永远自动登录第一个用户 $user_id = (int) session('user_id'); $user = Db::name('user')->field('password', true)->find($user_id); if ($user) { // 使用乐观锁检测/刷新VIP状态信息 // 返回1说明当前这里就是持有旧数据的了,要注意乐观锁安全是指它本身,而不是其他地方也是一样,其他地方还是需要自己去控制的 if (checkVip($user_id)) { $user = Db::name('user')->field('password', true)->find($user_id); } session('user_id', $user['id']); session('user', $user); $auth = [ 'id' => $user['id'], // 'nickname' => $user['nickname'], // 如果用户名修改则需要重新设置签名认证,不然会出现登陆失效 'auth_sign_key' => config('auth_sign_key'), ]; // TODO: 做得完善点这里认证不通过应该被系统退出,并且有退出原因,这个做登陆记录时需要考虑的 if (session('user_auth_sign') == dataAuthSign($auth)) { Session::pause(); return $user['id']; } else { Session::pause(); // 退出说明 } } session('user_id', null); session('user', null); session('user_auth_sign', null); Session::pause(); return 0; } ``` 并发不安全的原因就在于,一个进程持有了旧数据而进行操作而发生的并发问题,产生不符合业务逻辑预期的后果,也就是BUG,这种BUG根据不同的场景有不同的级别,最严重的就是钱变少了,所以任何时候,都要考虑你当前会不会持有旧数据,持有就数据之后你做的操作会带来那些问题,任何时候都要考虑并发问题。**而乐观锁的sql决定了它不会持有旧数据再去做什么(在更新的同时检测是否持有旧数据了,旧数据为更新的条件,如果真旧了也就不满足更新条件而不能更新了),所以它本身是安全的,注意是它自己本身,如上面的checkVip内部的操作是安全的的,但是其他地方还需要自己去进行控制。** >[danger] **你永远无法不知道自己哪一刻会持有旧数据(除非使用悲观锁)**,而乐观锁的聪明之处就是在自己做更新操作的时候问自己,最后确定一遍自己是不是安全的,是不是没有持有旧数据,确保只有当前没有持有旧数据才可以进行更新操作,所以它是安全的,也是聪明的。 * * * * * ### 相关阅读 [并发问题带来的后果](https://www.kancloud.cn/xiak/php-node/347803) [查看这个重要的提交 - 用乐观锁刷新会员状态在用户列表的使用](https://coding.net/u/xiasf/p/zb/git/commit/3732ba2cb0c50b500578469c093a6b39eb1e2b6c) [Mysql乐观锁悲观锁行锁表锁是从哪几个方面来分类的?比如说表锁也可是乐观锁,也可以是悲观锁吗?](https://segmentfault.com/q/1010000003917591) ~~~ 加锁对并发访问的影响体现在锁的粒度上,可见行锁粒度最小,并发访问最好,页锁粒度最大,表锁介于2者之间。 锁有两种:悲观锁和乐观锁。悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好。 与悲观锁相反,乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能。但是如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,会增加并发用户读取对象的次数。 ~~~ [乐观锁与悲观锁各自适用场景是什么?](https://segmentfault.com/q/1010000002660301) [搜索 悲观锁 - SegmentFault](https://segmentfault.com/search?q=悲观锁) * * * * * [消息推送 - 乐观锁](https://coding.net/u/xiasf/p/zb/git/commit/6ffe08eef85564dece1af7c012ff7e2c412d3514) >[danger] 乐观锁能够成立的原因就是 **因为数据库对同一数据进行更新操作时,不是并行的,而是串行阻塞的。自动对符合WHERE条件的行加的排他锁)** 并且查询是查的最新的,所以能保证更新语句中的where能查询到最新的、确定的、稳定的、安全的数据。不然如果两条update能够同时执行的话,没有对共享数据上锁,那么where就会持有旧数据,就不会有乐观锁的效果了,乐观锁做的事就是在最后更新的那一步,拦截,检测是否持有旧数据了,如果持有旧数据则不满足更新条件。 > 这其实又绕到了即使乐观锁,其实也要上锁的问题上面去了,只不过这种方式利用了数据库的隐式锁——对数据进行更新时会自动对数据上锁(**这是编程语言底层约定俗成的规定吧,不然更新数据,更改变量不会发生数据错乱的情况吗?**)。JAVA CAS内部原理估计也是如此。 [JAVA CAS原理深度分析 - CSDN博客](http://blog.csdn.net/hsuxu/article/details/9467651) > 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。 >[danger] 可见,内部还是用的锁,那怕是到cpu层面,也是用的锁(CPU的锁),不然对共享变量读写时怎么保证数据的正确性,虽然我们平时写代码不用考虑这个问题,编程语言对于我们来说总是安全的,但是计算机内部其实也做了安全考量和优化,以保证我们认为的那些再正常和普通的操作得以正确的执行,只不过我们忽略掉了操作系统/编程语言内部为此所做的努力而已。 [小白科普:悲观锁和乐观锁 - 码农翻身](http://mp.weixin.qq.com/s/gWR1-511SAwVAHrtGrsZ8g) [爱炫耀的数据库老头儿](https://mp.weixin.qq.com/s/Tdh5fYu-2cPVd6PkpiyeBA) [select语句for update---转载 - 一天不进步,就是退步 - 博客园](https://www.cnblogs.com/davidwang456/archive/2013/08/20/3270101.html) > **FOR UPDATE [NOWAIT]** > nowait的含义很多人都会误解为“不用等待,立即执行”。但实际上该关键字的含义是“不用等待,立即返回” 如果当前请求的资源被其他会话锁定时,会发生阻塞,nowait可以避免这一阻塞,因为 If another user is in the process of modifying that row, we will get an ORA‐00054 Resource Busy error. We are blocked and must wait for the other user to finish with it. * * * * * ### for update 细节注意!!! 并不是查询(查询行数据)时被卡住阻塞,而是阻塞它查询,必须先持有锁(先持有where 范围内的行的锁),才能够进行查询。查询行数据前先获取了 where 行范围,检查锁的情况。 获得锁之前还没有执行查询(获取行数据)且被阻塞着,不让返回,要先获得锁(写锁/排它锁),才能查询。而如果当前锁被别人持有,那么就只能被阻塞着,直到别人释放锁。 如果当前没有被其他人占有锁,那么取得锁,进行查询,返回结果。同样的,在锁释放之前(事务提交/事务完成),其他人要想获得锁时也会被阻塞。 `select ... for update`:**获得范围内行的排它锁。** 所以这里关键点就是 **持有 where 范围内的锁**,如果where 范围内记录存在,申请获取获取这些行的锁,如果这些行还不存在,**申请间隙锁或next锁**,开发时尤其要注意这点,不然很容易出现死锁。 ```` // a select id from orders where id = 1 and status = 1 for update // b select id from orders where id = 1 and status = 2 for update 他们都要锁同一条记录,但遗憾的是,他们申请的是不同的 next 锁,这样导致都能同时获得锁,显然这并不符合我们想要的结果,并且 next 锁 还极有可能造成死锁。 所以业务中使用锁时,一定要明确锁定的行记录,只使用 where id,想要获得某种状态,下面自己再判断就是了,不要写在 where 中。 验证了,不会出现这个情况,应该是有主键的情况。果然是的,如果没有主键(id 换成 sn),就出现了我们担心的情况了。 如果主键不靠左呢?测试:无关乎左右。 所以 where 中一定要有主键。 ```` * * * * * [程序员进阶乐观锁与悲观锁](https://www.toutiao.com/a6491604587517051406/?tt_from=weixin&utm_campaign=client_share×tamp=1516012360&app=news_article&utm_source=weixin&iid=22069500288&utm_medium=toutiao_android&wxshare_count=1) ```sql update goods set num= newnum, version = oldversion+1 where version = oldversion; ``` > 乐观锁可实现是因为更新时是对数据上锁的,即在数据库里面,同一时刻,对同一行数据的更新,是阻塞串行的。不可能两个会话同时对同一数据进行更新。 > > 正因为如此,所以乐观锁where查询时数据是可靠的,最新的,能保证此时不会被别人更改了,所以才能保证乐观锁是可靠的。 >[tip] 上面说的不准确,乐观锁能成立的保障就是当前读啊。 [为什么开发人员必须要了解数据库锁?](https://www.toutiao.com/a6587983050582262279/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1×tamp=1536342441&app=news_article_lite&utm_source=weixin&iid=33124962994&utm_medium=toutiao_android&group_id=6587983050582262279) * * * * * >[danger] 要想保证数据正确,那么必须要保证同一时刻,只能有一个会话能对这行数据进行操作。 > >[danger] 如果要防止并发问题(下单查库存问题),则必须要保证,一个会话在更改这行数据时必须独占,必须阻塞其它会话对这行数据的访问,查询也不行,也要被阻塞着。因为当数据处于被更改的状态时,我们无法确定它到底是什么,它是不可知的,而“一致性非锁定读”是快照读,不能保证查出来的数据是最新可靠真实的,相当于从缓存中读出来的数据,你说缓存中的数据靠谱吗,所以用这个数据去下单,当然会有问题了。 ***** > 乐观锁成立是因为,读和写是不可分割的,这是在同一条sql语句中,中间没有空隙,是独占的。 * * * * * [请问 5.1.29 LTS 怎么实现乐观锁的功能? · Issue #1548 · top-think/framework](https://github.com/top-think/framework/issues/1548) ***** last update:2018-1-15 22:31:01
';