分布式锁
在同一个jvm进程内,我们可以使用synchronized或者ReentrantLock锁,来完成对共享资源的互斥访问。然而现在大多数系统都是分布式系统,jvm进程分布在不同的节点上,为了全局数据的一致性,这个时候就需要分布式锁了。
下面展示几种分布式锁的实现
- 数据库,使用悲观锁for update机制。
- Zookeeper,基于瞬时有序节点的特性
- Redis,基于Setnx实现分布式锁
数据库悲观锁
原理
通过select …for update访问同一条数据
实现
- 在数据库中建立分布式锁所需的表
CREATE TABLE `distributed_lock` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`business_code` varchar(50) NOT NULL COMMENT '业务代码',
`describe` varchar(255) NOT NULL COMMENT '描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式锁';
-- 模拟订单业务,添加数据
INSERT INTO `test_1`.`distributed_lock` (`id`, `business_code`, `describe`) VALUES (1, 'order', '订单锁');
- 在项目中添加for update语句,我这里演示是用的mybatis
/**
* 通过业务代码获取锁
* @param businessCode 业务代码
* @return
*/
@Select("select * from distribute_lock where business_code = #{businessCode} for update")
DistributeLock selectDistributeLock(@Param("businessCode") String businessCode);
- controller测试用例
/**
* 数据库分布式锁
* 一定要添加事务@Transactional,否则for update执行完毕后会自动释放锁
*/
@RequestMapping("dbLock")
@Transactional(rollbackFor = Exception.class)
public void dbLock() throws Exception {
log.info("我进入了dbLock方法!");
// 调用for update语句
DistributeLock distributeLock = distributeLockMapper.selectDistributeLock("order");
// 如果不为空则进入锁,为空代表已经有其它请求占住了锁
if (distributeLock == null) {
throw new Exception("分布式锁找不到");
}
log.info("我进入了锁!");
try {
// 模拟业务用时
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("执行完毕,释放锁!");
}
redis分布式锁
原理
基于Redis的Setnx实现分布式锁,利用NX的原子性。多个线程并发时,只有一个线程可以设置成功,设置成功即获得锁,可以执行后续的业务处理,如果出现异常,锁过期会自动释放。释放锁采用Redis的delete命令。释放锁时要校验之前设置的value,相同才可释放,防止释放了别人的锁,释放锁采用LUA脚本。
set [key] [value] NX PX 30000
- key:资源名称,可根据不同业务区分不同锁
- value:唯一值,用于释放锁时的校验
- NX :key不存在时设置成功,key存在时设置不成功
- PX:自动失效时间,出现异常情况,锁可以过期失效
实现
- 编写redisLock的封装
/**
* 实现AutoCloseable接口来自动释放锁
*/
@Slf4j
public class RedisLock implements AutoCloseable {
private RedisTemplate redisTemplate;
private String key;
private String value;
//单位:秒
private int expireTime;
public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
this.redisTemplate = redisTemplate;
this.key = key;
this.expireTime=expireTime;
//使用UUID作为value
this.value = UUID.randomUUID().toString();
}
/**
* 获取分布式锁
*/
public boolean getLock(){
RedisCallback<Boolean> redisCallback = connection -> {
//设置NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
//设置过期时间
Expiration expiration = Expiration.seconds(expireTime);
//序列化key
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
//序列化value
byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
//执行setnx操作
return connection.set(redisKey, redisValue, expiration, setOption);
};
//获取分布式锁
return (Boolean)redisTemplate.execute(redisCallback);
}
/**
* 释放锁
*/
public boolean unLock() {
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
List<String> keys = Collections.singletonList(key);
return (Boolean)redisTemplate.execute(redisScript, keys, value);
}
/**
* 自动释放锁
*/
@Override
public void close() throws Exception {
unLock();
}
}
- controller测试用例
@Autowired
private RedisTemplate redisTemplate;
/**
* redis锁
*/
@RequestMapping("redisLock")
public void redisLock() {
log.info("我进入了redisLock方法!");
// 使用自动释放锁
try (RedisLock redisLock = new RedisLock(redisTemplate, "order", 30)) {
// 获取锁
if (redisLock.getLock()) {
log.info("我进入了锁!!");
//模拟业务
Thread.sleep(20000);
}
} catch (Exception e) {
e.printStackTrace();
}
log.info("执行完毕,自动释放锁!");
}
zookeeper分布式锁
原理
基于zookeeper的瞬时有序节点的特性,多线程并发创建瞬时节点时,得到有序的序列,序号最小的线程获得锁。其他的线程监听自己序号的前一个序号,以此类推。
观察器:
1. 节点数据发生变化,发生给客户端
2. 观察器只能监控一次,再监控需要重新设置
实现
- 编写zkLock
/**
* 实现AutoCloseable接口来自动释放锁
* 实现Watcher接口来使用观察器
*/
@Slf4j
public class ZkLock implements Watcher, AutoCloseable {
private ZooKeeper zooKeeper;
private String businessCode;
private String znode;
public ZkLock(String connectString, String businessCode) throws IOException {
this.zooKeeper = new ZooKeeper(connectString, 30000, this);
this.businessCode = businessCode;
}
/**
* 获取锁
*/
public boolean getLock() throws KeeperException, InterruptedException {
// 判断业务根节点是否存在 /order
Stat existsNode = zooKeeper.exists("/" + businessCode, false);
if (existsNode == null) {
//不存在创建业务根节点,业务节点要设置为持久节点,是所有的锁要放在哪个节点下。
zooKeeper.create("/" + businessCode, businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, // 无需账号密码即可连接
CreateMode.PERSISTENT); // 创建持久节点
}
// 创建瞬时有序节点 /order/order_ ,zooKeeper会在_后自动加序号的
znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL); // 创建瞬时有序节点
// 拿到当前创建的节点的名称
znode = znode.substring(znode.lastIndexOf("/") + 1);
// 获取 /order 下所有的子节点
List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
// 对所有子节点排序
Collections.sort(childrenNodes);
// 第一个子节点
String firstNode = childrenNodes.get(0);
// 如果是第一个节点,则获得锁
if (!znode.equals(firstNode)) {
return true;
}
// 不是第一个节点则监听前一个节点
// 默认第一个节点为前一个节点
String lastNode = firstNode;
for (String node : childrenNodes) {
// 找到当前节点,对前一个节点进行监听
if (znode.equals(node)) {
zooKeeper.exists("/" + businessCode + "/" + lastNode, true);
break;
} else {
// 未找到设置当前节点为前一个节点
lastNode = node;
}
}
// 监听的时候开启线程等待
synchronized (this) {
wait();
}
return true;
}
/**
* 监听器
*/
@Override
public void process(WatchedEvent watchedEvent) {
//如果节点被删除,唤起线程
if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
synchronized (this) {
notify();
}
}
}
/**
* 释放锁
*/
@Override
public void close() throws Exception {
zooKeeper.delete("/" + businessCode + "/" + znode, -1);
zooKeeper.close();
}
}
- controller测试用例
/**
* zookeeper锁
*/
@RequestMapping("zkLock")
public void zkLock() {
log.info("我进入了zkLock方法!");
// 使用自动释放锁
try (ZkLock zkLock = new ZkLock("localhost:2181", "order")) {
// 获取锁
if (zkLock.getLock()) {
log.info("我进入了锁!!");
//模拟业务
Thread.sleep(20000);
}
} catch (Exception e) {
e.printStackTrace();
}
log.info("执行完毕,自动释放锁!");
}
优缺点
方式 | 优点 | 缺点 |
---|---|---|
数据库 | 实现简单、易于理解 | 对数据库压力大 |
Redis | 易于理解 | 不支持阻塞 |
Zookeeper | 支持阻塞 | 要理解Zookeeper,编写复杂 |
Curator | 提供锁的方法 | 依赖Zookeeper,强一致 |
Redisson | 提供锁的方法,可以支持阻塞 |
总结
- 可以自己写代码研究,但项目中不推荐自己编写分布式锁
- 推荐使用Redisson和Curator实现的分布式锁