# 分布式锁
**何为分布式锁?**
- 当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数
- 用一个状态值表示锁,对锁的占用和释放通过状态值来标识
**分布式锁的特点**
- **互斥性**:和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥
- **可重入性**:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁
- **锁超时**:和本地锁一样支持锁超时,防止死锁
- **高性能和高可用**:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级
- **支持阻塞和非阻塞**:和ReentrantLock一样支持lock和trylock以及tryLock(long timeout)
- **支持公平锁和非公平锁(可选)**:公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的
**三种方案对比**
- **从理解的难易程度角度(从低到高)**:数据库 > 缓存 > Zookeeper
- **从实现的复杂性角度(从低到高)**:Zookeeper >= 缓存 > 数据库
- **从性能角度(从高到低)**:缓存 > Zookeeper >= 数据库
- **从可靠性角度(从高到低)**:Zookeeper > 缓存 > 数据库
## MySQL
### 基于唯一索引(`insert`)实现
**记录锁的乐观锁方案**。基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含**方法名**等字段,并在**方法名字段上创建唯一索引**,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
#### 优缺点
**优点**
- 实现简单、易于理解
**缺点**
- 没有线程唤醒,获取失败就被丢掉了
- 没有超时保护,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁
- 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用
- 并发量大的时候请求量大,获取锁的间隔,如果较小会给系统和数据库造成压力
- 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错,没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作
- 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁,因为数据中数据已经存在了
- 这把锁是非公平锁,所有等待锁的线程凭运气去争夺锁
#### 实现方案
```mysql
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`lock_key` varchar(64) NOT NULL DEFAULT '' COMMENT '锁的键值',
`lock_timeout` datetime NOT NULL DEFAULT NOW() COMMENT '锁的超时时间',
`remarks` varchar(255) NOT NULL COMMENT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_lock_key` (`lock_key`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
```
**① 获取锁**:想要执行某个方法,就使用这个方法名向表中插入数据
```mysql
INSERT INTO method_lock (lock_key, lock_timeout, remarks) VALUES ('methodName', '2021-07-19 18:20:00', '测试的methodName');
```
**② 释放锁**:释放锁的时候就删除记录
```mysql
DELETE FROM method_lock WHERE lock_key ='methodName';
```
#### 问题与解决
- 强依赖数据库可用性,是一个单点(部署双实例)
- 没有失效时间,一旦解锁失败,就会导致死锁(添加定时任务扫描表)
- 一旦插入失败就会直接报错,不会进入排队队列(使用while循环,成功后才返回)
- 是非重入锁,同一线程在没有释放锁之前无法再次获得该锁(添加字段记录机器和线程信息,查询时相同则直接分配)
- 非公平锁(建中间表记录等待锁的线程,根据创建时间排序后进行依次处理)
- 采用唯一索引冲突防重,在大并发情况下有可能会造成锁表现象(采用程序生产主键进行防重)
### 基于表字段版本号实现
**版本号对比更新的乐观锁方案**。一般是通过为数据库表添加一个 `version` 字段来实现读取出数据时,将此版本号一同读出。之后更新时,对此版本号加 `1`,在更新过程中,会对版本号进行比较,如果是一致的,没有发生改变,则会成功执行本次操作;如果版本号不一致,则会更新失败。实际就是个`CAS`过程。
#### 优缺点
**缺点**
- 该方式使原本一次的update操作,必须变为2次操作:select版本号一次、update一次。增加了数据库操作的次数
- 如果业务场景中的一次业务流程中,多个资源都需要用保证数据一致性,那么如果全部使用基于数据库资源表的乐观锁,就要让每个资源都有一张资源表,这个在实际使用场景中肯定是无法满足的。而且这些都基于数据库操作,在高并发的要求下,对数据库连接的开销一定是无法忍受的
- 乐观锁机制往往基于系统中的数据存储逻辑,因此可能会造成脏数据被更新到数据库中
### 基于排他锁(`for update`)实现
**基于排它锁的悲观锁方案**。通过在select语句后增加`for update`来获取锁,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁,我们可以认为获得排它锁的线程即可获得分布式锁。释放锁通过`connection.commit();`操作,提交事务来实现。
#### 优缺点
**优点**
- 实现简单、易于理解
**缺点**
- 排他锁会占用连接,产生连接爆满的问题
- 如果表不大,可能并不会使用行锁
- 同样存在单点问题、并发量问题
#### 实现方案
**建表脚本**
```mysql
CREATE TABLE `methodLock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`lock_key` varchar(64) NOT NULL DEFAULT '' COMMENT '锁的键值',
`lock_timeout` datetime NOT NULL DEFAULT NOW() COMMENT '锁的超时时间',
`remarks` varchar(255) NOT NULL COMMENT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY ( `id` ),
UNIQUE KEY `uidx_lock_key` ( `lock_key ` ) USING BTREE
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '锁定中的方法';
```
**加解锁操作**
```java
/**
* 加锁
*/
public boolean lock() {
// 开启事务
connection.setAutoCommit(false);
// 循环阻塞,等待获取锁
while (true) {
// 执行获取锁的sql
String sql = "select * from methodLock where lock_key = xxx for update";
// 创建prepareStatement对象,用于执行SQL
ps = conn.prepareStatement(sql);
// 获取查询结果集
int result = ps.executeQuery();
// 结果非空,加锁成功
if (result != null) {
return true;
}
}
// 加锁失败
return false;
}
/**
* 解锁
*/
public void unlock() {
// 提交事务,解锁
connection.commit();
}
```