![](https://img.haomeiwen.com/i13583191/87176f37e04bd1bd.png)
什么是锁?
如图所示:现实生活中的锁。
名词解释:锁是一个汉字,读作suǒ,本意是指置于可启闭的器物上,以钥匙或暗码开启,引申义是用锁锁住、封闭。该文字在《大铁椎传》和《论衡·订鬼篇》等文献均有记载。
锁是私有化的产物,什么意思呢?
它的作用在于 自行车之所以是你的,是因为你可以对自行车上锁,在你想要使用的时候,拿钥匙开锁,而别人无法随意骑你的自行车,那么自行车就是你私有的。同理,门锁,手机屏锁亦是如此。
作为一个混迹在代码底层的码农来说,锁的意义更加非凡。下面就来讲讲我与锁故事~~
ps:这里只说mutex锁!
故事?事故?
时间:2019年10月-2019年11月
任务:售后重构
这个任务是我来玩物接到的第一个大活,涉及到整个售后流程的改动与优化,因此我们重构了整个售后,包括底层的表结构也是新起了一张表,整个售后流程的改动优化是痛苦的,但也是收获巨大的。
售后的开发中,存在如下场景:
一、场景1
![](https://img.haomeiwen.com/i13583191/698dbc2769c7df5b.png)
如上场景1是一个正常的买家下单、申请售后,卖家同意售后,确认退款的流程。
二、场景2
![](https://img.haomeiwen.com/i13583191/2d1e299addc6edbb.png)
如上场景2是买家申请售后以后,自己又取消售后的流程,看似也没毛病。
三、场景3
![](https://img.haomeiwen.com/i13583191/6c5332400a60dd48.png)
如上场景3,模拟了一个并发的场景,由于买家申请售后以后,售后单的状态有两个可允许的操作,卖家A同意售后,确认退款 或者 买家B取消售后,当在time4时刻,卖家与买家对同一笔售后进行了操作,会怎么样呢?
可能出现多种情况:
![](https://img.haomeiwen.com/i13583191/462f9cbc29ed3a04.png)
如上情况1,卖家同意收货,确认退款操作成功,提示买家取消售后操作失败。最终结果,退款成功!也是正常的!
![](https://img.haomeiwen.com/i13583191/ca5d79ce6f8a628a.png)
如上情况2,买家取消售后成功,提示卖家操作失败,最终结果,售后取消,订单状态恢复为已付款!正常!
![](https://img.haomeiwen.com/i13583191/ce5141a35d9a9d0e.png)
如上情况3,出现了退款成功,但是售后却被取消,订单恢复成已付款的情况,这是一种异常情况,在买家与卖家同时操作同一笔售后的时候就有可能会发生!
那么如何规避这种异常情况呢,关键的点在于同一时刻只能有买家或者卖家其中的一个人来操作同一笔售后,这里就需要用到锁,确切的说是互斥锁,即同一时刻只允许拿到锁的线程进行操作,另一个线程只能等待锁释放后才有机会拿到锁进行操作。
单机锁?
对于锁的了解,就要追溯到刚毕业那时候,那时候对于锁,我大概只知道synchronized,并把它奉之为万能的法宝,凡是涉及到线程安全的问题,都是小脑瓜一转,小手一敲,就是一个synchronized。
直到后来过了很久很久我才知道有ReentrantLock之类的东西。
这两个都是单机应用级别的锁,在传统的互联网行业中可能用的比较频繁,单机应用并发访问都不高的情况下,用着两个很方便也几乎不需要什么代价。
对于这两个锁的区别与应用方法在这里不展开了,后续在单独开篇讲讲。
毕竟我们都是分布式的场景,单机的锁必然没法达到我们期望的结果。
分布式锁?
分布式锁有三种实现方法:
一、基于数据库
基本的实现如下,创建一张表,为表中的业务字段创建唯一索引,当对于同一条售后发起多个请求时,先往表里插入一条数据,插入成功的则认为获取锁,可执行具体的操作,执行完毕,删除数据库中的记录,即为释放锁。
建表示例:
CREATE TABLE `dbLock` (
`id` bigint NOT NULL AUTO_INCREMENT,
`lockName` varchar(255) DEFAULT NULL COMMENT '锁名',
`ip` varchar(255) DEFAULT NULL COMMENT '获得锁的机器IP',
`thread` varchar(255) DEFAULT NULL COMMENT '获得锁的线程id',
`lockTime` bigint DEFAULT NULL COMMENT '加锁时间',
`expireTime` bigint DEFAULT NULL COMMENT '失效时间',
`desc` varchar(255) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`),
UNIQUE KEY `lockName` (`lockName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
优势:
简单方便,直接利用数据库的锁来实现互斥。
劣势:
1、分布式锁的可用性与性能取决于数据库本身,开销比较大
2、没有锁失效机制,即插入一条数据获得锁后,如果出现宕机,那么这条数据不会被删除,别的线程无法再获取到锁。【可以再每条记录上记录一个失效时间的字段,通过定时任务去扫】
二、基于zookeeper
实现1.
基于单个临时节点的创建,创建成功即获取到锁,创建失败说明有线程已获取到锁,此时只需监听这个节点的变更事件,当获取锁的线程通过删除节点来释放锁时,其他监听这个节点的线程就会尝试去创建节点获取锁。
优势:
临时节点在客户端宕机时自动删除,不会出现死锁的情况。
存在一个著名的问题:羊群效应。
大量线程监听zookeeper的节点,当节点被删除,就不得不发出大量的通知,当这个量非常大时,可能会出现延迟,网络消耗;此外线程获得锁的成功率非常低,效率不高,但是每次又要去争抢着创建,非常消耗性能。说白了就是会做很多无用功。
![](https://img.haomeiwen.com/i13583191/b1b545d40cf6ade1.png)
实现2.
基于临时顺序节点。创建一个lock目录,每个线程请求进来时,在lock目录上创建临时顺序节点,判断是不是最小的那个节点,如果是就获取到锁直接执行。如果不是就监听上一个节点的变更事件。如此,每个节点被删除的时候只会通知下一个节点去获取锁,就不存在上面说的羊群效应。
![](https://img.haomeiwen.com/i13583191/8aa695366f62edfc.png)
三、基于redis
实现思想:
(1)、获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁[也可能出现setnx加锁后还没来得及执行expire命令就宕机的情况,因此通常可以用lua表达式将两条命令合成一个语句来实现两个操作的原子性,local rs=redis.call('setnx',KEYS[1],ARGV[1]);if(rs<1) then return 'F';end;redis.call('expire',KEYS[1],tonumber(ARGV[2]));return 'S'],锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)、获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)、释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
优势:redis有很高的性能,且命令支持实现起来容易。
![](https://img.haomeiwen.com/i13583191/7a96b0fdc978596a.png)
技术选型?
我们售后分布式锁选择了redis来实现,主要考虑到其性能较好,实现也比较容易,可以通过现成的api来操作,通过简单的切面织入即可应用生产环境。
网友评论