分布式锁——学习流程
1.什么是分布式锁
2.分布式锁的应用场景
3.怎么实现分布式锁
4.分布式锁是如何运作的
回答
1.在微服务应用中,为了保证最终一致性,我们一般采用分布式事务,分布式锁,其中分布式锁就是在 分布式系统多线程、多进程并且分布在不同机器上,使的原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁
分布式锁,需要满足CAP理论:即一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能三选二,只有两种解法:PC或者AP
2.
(一)高并发场景下的数据一致性
在高并发的电商系统中,商品库存的扣减是一个关键操作。如果多个用户同时购买同一件商品,没有分布式锁的情况下,可能会出现库存超卖的问题。使用分布式锁可以确保在同一时间只有一个线程能够对库存进行扣减操作,从而保证数据的一致性。
(二)分布式任务调度
在分布式任务调度系统中,可能会有多个节点同时执行相同的任务。为了避免重复执行,需要使用分布式锁来保证只有一个节点能够获取到任务并执行。例如,在分布式定时任务系统中,使用分布式锁可以确保每个任务在同一时间只被一个节点执行。
(三)分布式事务中的资源锁定
在分布式事务中,可能需要对多个资源进行锁定,以保证事务的原子性和一致性。分布式锁可以用于锁定这些资源,确保在事务执行过程中,其他节点无法对这些资源进行修改。例如,在分布式银行系统中,转账操作可能需要锁定转出账户和转入账户,以确保转账金额的正确转移。
3。三种方式实现
基于数据库实现分布式锁; 基于缓存(Redis等)实现分布式锁; 基于Zookeeper实现分布式锁;
一:基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
(1)创建一个表:
DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',`desc` varchar(255) NOT NULL COMMENT '备注信息',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`id`),UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
(2)想要执行某个方法,就使用这个方法名向表中插入数据:
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');
因为我们对method_name
做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
(3)成功插入则获取锁,执行完成后删除对应的行数据释放锁:
delete from method_lock where method_name ='methodName';二:(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete
delete key:删除key
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.Response;
import redis.clients.jedis.exceptions.WatchErrorException;
import java.util.UUID;
public class RedisDistributedLock {
private Jedis jedis; // Redis 客户端实例
private String password; // Redis 访问密码(若无密码可设为 null)
private int dbIndex = 10; // Redis 数据库索引
// 构造方法初始化 Redis 连接
public RedisDistributedLock(String host, int port, String password) {
this.jedis = new Jedis(host, port);
this.password = password;
if (password != null) {
jedis.auth(password); // 认证密码
}
jedis.select(dbIndex); // 选择数据库
}
/**
* 获取分布式锁
* @param lockName 锁名称
* @param acquireTime 最大等待时间(秒)
* @param timeout 锁超时时间(秒)
* @return 锁标识符(获取失败返回 null)
*/
public String acquireLock(String lockName, int acquireTime, int timeout) {
String identifier = UUID.randomUUID().toString(); // 生成唯一标识符
String lockKey = "string:lock:" + lockName; // 构造完整锁键
long endTime = System.currentTimeMillis() + acquireTime * 1000L;
while (System.currentTimeMillis() < endTime) {
if (jedis.setnx(lockKey, identifier) == 1) { // SETNX 原子操作尝试获取锁
jedis.expire(lockKey, timeout); // 设置锁超时(防止死锁)
return identifier; // 成功获取锁
}
// 若锁未设置超时,主动重置超时(防止其他客户端未设置)
if (jedis.ttl(lockKey) == -1) {
jedis.expire(lockKey, timeout);
}
try {
Thread.sleep(1); // 降低 CPU 占用,控制重试频率
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return null; // 超时未获取锁
}
/**
* 释放分布式锁(事务安全)
* @param lockName 锁名称
* @param identifier 锁标识符(需与获取时一致)
* @return 是否释放成功
*/
public boolean releaseLock(String lockName, String identifier) {
String lockKey = "string:lock:" + lockName;
while (true) {
jedis.watch(lockKey); // 开启事务监视
// 验证当前锁值是否为本客户端的标识符
String currentValue = jedis.get(lockKey);
if (identifier.equals(currentValue)) {
Transaction tx = jedis.multi(); // 开启事务
tx.del(lockKey); // 删除锁键
try {
if (tx.exec() != null) { // 提交事务(成功返回非空)
return true;
}
} catch (WatchErrorException e) {
// 事务期间锁键被修改,重试
}
} else {
jedis.unwatch(); // 标识符不匹配,取消监视
break;
}
}
return false;
}
// 关闭 Redis 连接(重要!避免资源泄漏)
public void close() {
if (jedis != null) {
jedis.close();
}
}
// 示例用法
public static void main(String[] args) {
RedisDistributedLock lock = new RedisDistributedLock("localhost", 6379, "your_password");
String lockId = lock.acquireLock("order_lock", 10, 5); // 10秒内获取锁,超时5秒
if (lockId != null) {
try {
System.out.println("执行业务逻辑...");
} finally {
boolean released = lock.releaseLock("order_lock", lockId);
System.out.println("锁释放结果: " + released);
}
} else {
System.out.println("获取锁失败");
}
lock.close(); // 释放连接
}
}
三:ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。(这个我用的少,不太会)