多线程下的分布式锁技术

举报
tea_year 发表于 2023/12/09 09:59:58 2023/12/09
【摘要】 1. 分布式锁的实现方式(1) 使用mysql实现基于mysql的唯一索引。使用mysql的唯一索引做添加功能,加锁的时候,判断此唯一索引字段是否插入成功,如果成功则证明获取到了锁,如果添加失败则证明获取锁失败。解锁操作:删除此条记录即可。模拟代码:/** * 基于mysql的分布式锁:加锁 * @param username 唯一索引的字段 * @return true:获取锁 false...

1. 分布式锁的实现方式

(1) 使用mysql实现

基于mysql的唯一索引。

使用mysql的唯一索引做添加功能,加锁的时候,判断此唯一索引字段是否插入成功,如果成功则证明获取到了锁,如果添加失败则证明获取锁失败。

解锁操作:删除此条记录即可。

模拟代码:

/**
* 基于mysql的分布式锁:加锁
* @param username 唯一索引的字段
* @return true:获取锁 false:没有获取锁
*/
public boolean lock(String username){
int i = 0;
try {
i = insert into user (username) values (#{username});
} catch (Exception e) {
e.printStackTrace();
}
return i>0;
}

/**
* 解锁操作
*/
public void unlock(String username){
delete from user where username=#{username}
}



使用案例:

public void buy(Product product,String username){
try {
if (lock(username)){//加锁
//业务逻辑...
}
} catch (Exception e) {
e.printStackTrace();
} finally {
unlock(username);//释放锁
}
}




(2) 使用zookeeper实现

基于zookeeper临时有序节点可以实现的分布式锁。每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

实现步骤:

(1)创建一个目录mylock;
    (2)线程A想获取锁就在mylock目录下创建临时顺序节点;
    (3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
    (4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
    (5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

(3) 使用redis实现
加锁操作:

基于redis的set key val ex|px time nx 的命令是可以实现分布式锁的。ex的意思是设置一个key并且赋予生命周期,nx再生命周期结束之前是不允许第二个key进行设置的,所以nx保证了互斥性,ex保证了不会死锁,利用这个特性可以实现分布式锁(幂等性),如下图所示,只要第二次是相同的key就不允许插入数据:

不加超时时间为什么会产生死锁?

如果一个应用获取到锁后,突然宕机,就会导致锁得不到释放,所以要加一个过期时间,防止死锁的发生。

解锁操作:

由于java程序是多线程的,所以在解锁的时候要保证释放的是线程自己的锁。也就是所谓的保证原子性。推荐使用lua脚本,lua脚本可以判断在delete key的时候是否是当前线程的,如果不是则不做操作。

场景:当线程A在获取锁执行的时候,当还没有执行到释放锁的操作的时候,锁的有效时间已经过期,线程A挂起。线程B获取分布式锁继续执行,当B执行一半的时候,时间片结束,线程A运行且要释放锁,此时释放的就是B的锁。这就是为什么要使用LUA脚本的原因。

加锁代码:


解锁代码:


Redis分布式锁工具类:

/**
* 分布式锁工具类
*/
public class LockUtil {

/**
* 加锁操作
* @param key 要上锁的key
* @param val
* @param timeOut 锁的失效时间,以秒为单位
* @return true:获取锁成功,false:获取锁失败
*/
public static boolean lock(String key,String val,int timeOut){
Jedis jedis = new Jedis("127.0.0.1", 6379);
/*
* set key value NX EX time:能保证在规定时间范围内有且只有一个命令可以set成功,
* 为了防止发生服务失败导致redis锁不被释放,造成死锁现象,所以加一个过期时间,防止
* 死锁的发生。
*/
String result = jedis.set(key, val, "NX", "EX", timeOut);
jedis.close();
if (result.equals("OK"))
return true;
else
return false;
}

/**
* 解锁操作
* @param key 上锁的key
* @param value
* @return true:解锁成功 false:失败
*/
public static boolean unlock(String key,String value){
Jedis jedis = new Jedis("127.0.0.1",6379);
/*
* luaScript 这个字符串是个lua脚本,代表的意思是如果根据key拿到的value
* 跟传入的value相同就执行del,否则就返回0,保证安全性;
*/
String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
/*
* redis.eval(String,list,list);这个命令就是去执行lua脚本,KEYS的集
* 合就是第二个参数,ARGV的集合就是第三参数,保证解锁的原子操作
*/
Object var2 = jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value));
jedis.close();
//如果释放锁成功,会返回1,失败会返回0.所以判断是否等于1.
if ("1".equals(var2.toString())){
System.out.println("解锁成功!!!");
return true;
}
return false;
}
}


测试:

public class MyRun implements Runnable{
int tickets;//票数

public MyRun(int tickets){
this.tickets = tickets;
}
@Override
public void run() {
ticket();
}
//抢票
public void ticket(){
String name = Thread.currentThread().getName();
try {
if (LockUtil.lock("tickets", name,30)){
if (tickets<=0){
System.out.println(name+"===================>>>对不起票卖完了!!");
return;
}
tickets--;
System.out.println(name+"恭喜您,抢到了第"+(10-tickets)+"张票!!!");
}
} catch (Exception e) {
} finally {
LockUtil.unlock("tickets",name);
}
}
}

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。