为什么需要分布式锁?
随着并发量的不断增加,单机的服务迟早要向多节点或者微服务进化,这时候原来单机模式下使用的synchronized或者ReentrantLock将不再适用,我们迫切地需要一种分布式环境下保证线程安全的解决方案,一般来说主要有三种,第一种是DB数据库的分布式锁,第二种是缓存的分布式锁,第三种是zookeeper实现的分布式锁。
基于数据库实现分布式锁
基于UNIQUE KEY
CREATE TABLE `database_lock` (`id` BIGINT NOT NULL AUTO_INCREMENT,`resource` int NOT NULL COMMENT '锁定的资源',`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',PRIMARY KEY (`id`),UNIQUE KEY `uiq_idx_resource` (`resource`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';
需要获得锁的时候,带上唯一约束的字段
INSERT INTO database_lock(resource, description) VALUES (1, 'lock');
这里用UNIQUE KEY来做了唯一性约束,这样的话如果有多个请求同时到数据库的时候,只有一个操作可以成功,其他的都会报错。
缺点和优点和很明显:非阻塞的,插入失败就直接报错了,可以用for循环弄成阻塞的
非可重入的
没有失效时间,一旦释放锁的操作失败就会导致锁记录一直在数据库里
基于乐观锁
通过比较,比如可以设置一个version的字段,来进行版本比较,一致就执行操作,不一致就不执行。适合于并发量不高,写操作不频繁的场景。
基于悲观锁
在使用悲观锁的同时,我们需要注意一下锁的级别。MySQL InnoDB引起在加锁的时候,只有明确地指定主键(或索引)的才会执行行锁 (只锁住被选取的数据),否则MySQL 将会执行表锁(将整个数据表单给锁住)。
在使用悲观锁时,我们必须关闭MySQL数据库的自动提交属性(参考下面的示例),因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。
mysql> SET AUTOCOMMIT = 0; Query OK, 0 rows affected (0.00 sec) 12
这样在使用FOR UPDATE获得锁之后可以执行相应的业务逻辑,执行完之后再使用COMMIT来释放锁。
基于Redis实现分布式锁
SETNX
一个原子性操作SET name ljpSET name wy 执行完 value应该是wy
如果是SETNX name ljpSETNX name wy 执行完 value却是ljp
因为SETNX 如果key不存在的时候,才会设置value,如果已经存在了,就不会设置,还是原来的值。
基于此,如果在RedisTemplate中,可以使用redistTemplate.opsForValue().setIfAbsent(key,value),以此作为标志位,如果进入的线程发现为true,则继续下面的正常set操作,相当于拿到了锁,redis是单线程的,其他线程可以判断后也就是失败,可以通过lock 方法让使用tryLock获取锁失败的线程本地自旋转重试获取锁,这类似JUC里面的CAS,除此之外也要保证锁必须得释放掉,也就是拿到锁的线程得释放锁,delete即可,也可以设置key的过期时间。
基于Zookeeper实现分布式锁
简述:主要是依靠 zookeeper的节点,当一个客户端创建一个节点时,另外一个客户端无法创建同名的节点(相当于互斥),且临时节点会在结束后自动删除,不影响使用。
A创建lock临时节点,然后看是否成功如果,当前只有这个lock节点那就成功获取了锁,要不然的话就继续监听自己的上一个节点,然后继续等待到能获取锁的时候。
优缺点对比:
| 优点 | 缺点 | |
|---|---|---|
| SQL | 实现容易,容易理解 | 性能差,数据库压力大 |
| Redis | 理解简单 | 不支持阻塞 |
| Redission | 对Redis的扩展封装,性能好,操作简单 | |
| Zookeeper | 支持阻塞 | 操作复杂、理解起来不容易 |
| Curator | 依赖于Zookeeper,操作简单 |
