应用场景
在业务传统场景下,单机版本的并发控制我们可以利用sychronized
和retrantLock
来完成,但是在集群环境下,不同的的JVM如何完成并发控制呢?这就需要利用分布式锁。
实现方式
1. 基于数据库的唯一约束实现
2. 基于缓存如Redis实现
3. 基于ZooKeeper实现
一 基于数据库的实现
实现思路:对lockMethod施加唯一约束
DROP TABLE IF EXISTS `c_table_lock_info`;
CREATE TABLE `c_table_lock_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,,
`lockMethod` varchar(64) DEFAULT NULL COMMENT '锁定的方法名',
`remark` varchar(255) DEFAULT NULL COMMENT '备注信息',
`addTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`updTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_lockMethod` (`lockMethod`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系统锁信息';
在之后每次遇到需要加锁的方法的时候,先执行以下SQL
INSERT INTO c_table_lock_info(lockMethod,remark) VALUES('加锁的方法名','方法备注');
如果多个请求都需要调用此方法,当其中某一个请求插入成功时,即可认为该请求获取了锁,完成相应的业务逻辑,在处理完之后,调用以下SQL
DELETE FROM c_table_lock_info WHERE lockMethod='加锁的方法名';
此时,锁被释放,可以再次被使用
但是存在一些缺点急需优化
1. 基于数据库实现,数据库的性能会直接影响锁的性能
2. 不具备可重入的性质,若在未释放锁之前,该请求再次调用此方法,导致无法插入数据,获取锁失败,需要加相应的认证信息,确认你还是你,但你还是你的时候,直接把锁给你
3. 锁没有相应的失效机制,在服务宕机之后,数据库中仍然存在该数据,当请求再次过来的时候,没有请求能获取到锁,需要加上失效时间,一段时间之后,锁自动失效
4. 没有阻塞特性,需要优化获取锁的代码逻辑,不断的循环重试去获取锁
二 基于缓存如Redis的实现
Redis是单线程的,也就是说存在很多客人来放松的时候,永远只有一个来处理客人要求的各种服务
实现思路:基于其实现的分布式锁思路就是利用原子命令来完成public String setex(final String key, final int seconds, final String value) {
在保存某一个key的时候并为其设置expirTime
return (String)(new JedisClusterCommand<String>(this.connectionHandler, this.maxAttempts) {
public String execute(Jedis connection) {
return connection.setex(key, seconds, value);
}
}).run(key);
}
为了保证在这种情况下的可重入性,仍然需要编写代码封装set
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Map;
/**
* @Description
* @Author taren
* @DATE 2019/8/22 16:29
*/
public class RedisLockTest01 {
private ThreadLocal<Map> lockers = new ThreadLocal<>();
private Jedis jedis;
public RedisLockTest01(Jedis jedis) {
this.jedis = jedis;
}
private boolean _lock(String key) {
return jedis.set(key, "", "nx", "ex", 5L) != null;
}
private void _unlock(String key) {
jedis.del(key);
}
private Map<String, Integer> currentLockers() {
Map<String, Integer> refs = lockers.get();
if (refs != null) {
return refs;
}
lockers.set(new HashMap<>());
return lockers.get();
}
public boolean lock(String key) {
Map refs = currentLockers();
Integer refCnt = (Integer) refs.get(key);
if (refCnt != null) {
refs.put(key, refCnt + 1);
return true;
}
if (!this._lock(key)) {
return false;
}
refs.put(key, 1);
return true;
}
public boolean unlock(String key) {
Map refs = currentLockers();
Integer refCnt = (Integer) refs.get(key);
if (refCnt == null) {
return false;
}
refCnt -= 1;
if (refCnt > 0) {
refs.put(key, refCnt);
} else {
refs.remove(key);
this._unlock(key);
}
return true;
}
}
当然setnx的加锁方式也是有缺陷的,完全可以编写Lua脚本来实现,用redis的connection对象去执行脚本
三 基于ZK的实现
实现思路:利用ZK的目录树特性结构,即同一目录下只能有唯一的文件名
1. 创建一个用来实现锁的目录lock
2. A线程获取锁,就在lock目录下创建顺序节点
3. 遍历lock目录,获取所有的子节点,A发现自己就是最小节点,获得锁
4. B线程遍历lock目录,发现自己不是最小节点,因为此时A才是最小节点,B对A设置监听
5. A使用完之后,删除节点,此时B发现自己是最小节点,B获得锁