并发-分布式锁质量保障总结
并发-分布式锁质量保障总结
实现分布式锁的主流方法
三种主流方法:
-
基于数据库(MySQL)实现分布式锁 基于MySQL锁表 数据库版本号乐观锁 基于缓存(redis)实现分布式锁 基于zookeeper/etcd实现分布式锁
MySQL数据库表的乐观锁适用于读多写少的场景且共享资源为数据库的单行数据;MySQL表锁实现的锁一般都不推荐使用;ZooKeeper分布式锁虽然适用于大部分分布式场景,但是由于其实现复杂度相对较高以及需要额外引入中间件,在大部分业务场景中的应用比较少,而基于Redis的缓存分布式锁应用较为广泛;但是具体业务实现采用哪种类型的分布式锁,还是需要基于当前的业务特性来进行决定;
redis分布式锁注意事项
Redis通常可以使用setnx(key,value)函数来实现分布式锁。key和value就是基于缓存的分布式锁的两个属性,其中key表示锁id。setnx函数返回1表示获得锁,返回0表示其他服务器已经获得了锁;
redis key
-
全面梳理业务场景,对于同一共同资源,key要保持一致 key是识别共享资源的唯一键,key既要锁住当前共享资源又不能影响其他资源
锁释放
-
锁一定需要明确释放,try/finally结构加解锁,finally内释放锁 锁只能被加锁对象释放,避免出现下图问题,A加锁被B释放,导致C拿到锁
锁超时
-
一定要设置key超时间; 超时时间的设置一般来讲大于服务的最大执行时间即可,在种极端情况处理方式如下2种: 可以再开启一个线程,为当前超时时间续时,但增加了系统的复杂度; 将过期时间设置非常长,一定能保证逻辑在锁释放之前能够执行完成;此方案简单但是有缺陷,当遇到系统突发异常时,锁无法被释放,阻断业务执行,很有可能造成故障;
锁力度
如果针对某个共享资源的写是基于另外一个共享资源的值计算而来,那么锁的范围必须包含读共享资源;范围不包含读共享资源会导致脏读,最终导致数据的错误,如下图所示,Client B最终计算的B的结果就是错误的。
锁获取失败
由于其他线程已经获取到了锁,当前线程获取锁失败后有3种处理方式:
-
异常抛出让用户重试; 通过自旋再次进行抢锁; 发布订阅,订阅锁释放消息;
在并发度低的场景下异常抛出以及自旋抢锁都可以,在高并发场景下异常抛出和自旋抢锁都不可取。
MySQL数据库锁
数据库版本号乐观锁
在数据库的表中需要包含一个数字类型的字段version,读取数据时把version字段读出来,更新数据时判断当前version是否等于读取出来的version,并对当前version+1;如果等于就更新成功,不等于表示数据已过期更新失败。例如以积分体系为例,存在多种场景增加积分,通过乐观锁来保证数据的正确性。 乐观锁注意点:
-
where条件一定要命中索引(最好是主键索引或唯一索引),否则会锁表; update table set 必须包含version = version + 1; update 返回结果为0时,一定要根据业务场景进行相应的处理,自主重试或者抛异常;
基于MySql锁表
创建一张锁表,对临界资源做唯一性约束,通过增加一条记录对某一资源上锁,释放锁时删除记录;一般不推荐此种用法。