情景1:订单A和订单B同事进行操作,同时进行了库存的操作,比如仓储系统的发货操作(减库存),因为A和B包含的产品有重叠的部分,由于mysql是允许并发查询的,所以A可能读到B改之前的库存数,在进行库存修改造成数据错误,尤其是计算期初和结余时(有人说用乐观锁加版本号的形式,但是一个订单操作往往是一个事物中完成,需要操作很多sku的库存最后才commit,如果因为一个sku库存更新失败而回滚整个事物,和最早开始的文加锁就没区别了,还不如不操作更节省资源,所以比较好的方式是排它锁),此时如果加入简单文件锁可以保证A操作时B无法操作,避免数据错误,但是B很可能会等待超时失败,加入订单很多,重复很多,其中有订单操作时间又长,锁一个库的方式势必会造成等待和大量订单失败。
所以一个是将锁颗粒变小,减小锁的范围(每个库多个锁),二是及时解锁解除无效占用。
这里锁要满足以下原则:
1.排它(互斥),A上锁的部分B不可操作
2.不能死锁,或者有解决死锁的方法
3.谁上的锁谁解,A不能解B的锁
一、文件锁可以保证互斥
之前有个简单的文件锁方案,这次由于操作的单量比之前大很多,又要求速度,上锁就更精细了,不能只能到库。
所以领导决定将每个库分成多份,比如100份,也就是100个锁,这样可以提高精细度,不至于一次锁一个库,其他人都操作不了。怎么实现呢,方案就是用“库Id+sku_id%100"作为文件名字,创建这个文件就锁住了所有sku在这个范围内的,
但是这样就会产生一个问题,上锁颗粒度是小了,但是多个订单拥有的sku大概率会有有重叠的部分,比如
订单1:sku:1 2 3 4 5 6 7 8 9
订单2:sku: 5 6 7 10 11 12
订单3:sku: 5 6 7 15 19
。。。
如果订单1把 5 锁住的时候 订单2把6锁住了,订单1再拿6时会失败,订单2再拿5时也会失败,相互都拿不到对方的锁,又拥有对方的锁,造成死锁(互卡),订单3也是一样的情况。为了解决死锁问题,我决定使用redis
缓存解决。
本来我想的是只用redis作上锁,比如以"sku+库id"做为key(也可以互斥),这样可以让颗粒度更小,直接类似于mysql的行锁,但是老板说只依赖redis他不放心,万一redis挂了或者不好用了,他还是钟情于之前的文件锁,遇到问题到时候直接删文件就好了,并且由于文件就在项目服务器中,避免了跨服务器网络请求,会更快。我说其实redis可以做持久化和高可用的。但是为了让老板放(饭)心(碗),啥活也得接不是,那我们就做一个使用文件上锁,用redis解锁的方案。
大体思路就是:1、利用系统不允许同一路径下 同时创建相同名称的文件(这个不用解释吧,linux和windows都不行的),这就是锁,假如我们有两台linux服务器,在A服务器指定一个目录存放”锁文件",并将这个目录和另一个服务器共享,将B服务器同样位置的目录指向(映射,比如做NAS)A的目录,这样就实现两台服务器一个锁,可以实现分布式锁。2、利用redis做缓存,将上锁成功和失败的信息存储进去,然后根据信息做解锁处理,再删除信息,这种反复存储redis缓存的效率高而且是单线程(redis是串行处理命令的),可以省很多事(比如处理并发问题)。
那么流程图如下:
https://download.csdn.net/download/xiaobai1024/13211827
说明:中心思想就两个方法,1:上锁,2:解锁。主要逻辑在于上锁,上锁时会将成功和失败的订单信息和产品信息存入redis,然后利用redis中的信息进行判断解锁(解锁策略)。
二、不能死锁
从流程图上看,不死锁的方法有两个,一个是超时解锁,一个是及时解锁。
超时解锁不难理解,假如一个订单10个sku,当成功上了8个锁,失败的两个还要循环去请求上锁,也就是订单还在处理中的状态,但是不能无限等待下去,因为它自己也占了8个锁,所以会有超时时间 ,超过一定时间则会处理失败,释放掉它的锁,允许其他订单操作。这个策略不区分产生死锁还是没有得到锁。
所谓及时解锁其实是放弃加锁,在产生死锁时使用,因为要满足第三个原则(谁上的锁谁解),所以在加锁的方法里加入主动放弃的策略,即释放自己锁,本次加锁失败。但是如何判断是否放弃还是继续请求加锁呢,就要基于redis中的数据进行权重计算,1.谁加锁加的多谁有权利继续加锁,少的放弃,(加锁比例)2.比例相同,谁先加锁谁有权利继续。
通过以上两个策略避免了死锁,当A订单没有和B产生死锁,A会等待B处理完毕处理,若B时间过长,A会因超时放弃。多个订单也是一样道理。如果A和B在上锁时发生互卡,他们自己必须判断是否要放弃加锁,最终会有一方放弃,即使判断时有一个线程坏了(比如重启服务器),另一方也会因超时解锁。
那么源代码如下:
https://download.csdn.net/download/xiaobai1024/13634109
问题:
一。加入文件锁,代码稍显复杂。逻辑设计也复杂了。
二。如果线程挂起或者崩溃,那么会有文件锁残留,需要手动删除才能解除。
所以当时我是想直接用redis的,毕竟redis有设置超时时间的方式,超过时间可以自动失效,就不用考虑残留问题。但这不是问题,将方法稍加改造就能实现了,需要注意的是redis的高可用和持久化问题。