分布式锁—-数据库和redis实现分布式锁

分布式锁—-数据库和redis实现分布式锁

前言:

在博客“zookeeper实现分布式锁的两种方式”中介绍了分布式锁的使用场景,以及如何用zookeeper分别实现简单和高性能的分布式锁,这里就不再重复介绍分布式锁的场景,今天主要给大家带来另外两种实现分布式锁的方式–数据库、redis

一、分布式锁实现的原理:

实现分布式锁的原理基本上就是相似的,使用第三方工具做到一个互斥(排它)的作用,比如:

1、zookeeper:当客户端向zk写入节点时,如果写入成功,其他的客户端就无法写入成功,可以理解为互斥

2、数据库:向数据库插入一条数据(比如用id主键,或者唯一索引)等达到其他的客户端无法再插入相同的数据

3、redis:当一个客户端向缓存中写成功一个key-value时,其他的客户端不能在写入相同的key

知道上面的原理,实现起来就很简单了。解锁就是分别删除他们创建的节点或者数据,其他的客户端就能重新创建该节点或者数据

二、使用mysql实现分布式锁

由于mysql实现分布式锁的性能非常非常差,根本不能在线上环境使用(如果你不怕被研发经理打死可以试一下),这里就详细的说一下mysql实现的思路,具体就不用代码实现

(1)新建一张表lock

该表可以只有一个字段id,当然是主键咯,保证唯一性

(2)加锁

加锁就是在java代码中向上面的数据库中插入一条数据insert into lock (id)values (1)

如果插入成功则表示获取到锁,否则就是获取锁失败,因为就一条sql,所以这也是原子性的(加锁和解锁必须保证原子性)

(3)解锁

解锁就是删除刚才插入的数据delete from lock where id = 1

就这么简单就能使用mysql实现分布式锁,大家可以试一下

三、redis实现分布式锁

这是本文的重点,所以会详细介绍,并且会用代码实现。在实现之前,我们先考虑一下在实现的过程中应当要注意什么

(1)加锁和解锁必须保证原子性

(2)谁加的锁,应当谁解锁,不能解别人的锁

(3)当发生死锁,或者某个客户端持有的锁一直不是放怎么办?锁过期

带着这三个问题,我们再来思考下如何具体实现

1、加锁:

(1)向缓存中写入一条数据,如果该key存在则写入失败,否则写入成功,

(2)另外要设置锁的过期时间,防止一直持有锁

(3)保证(1)和(2)必须是原子性,否则有可能失效了别人的锁

jedis.set(final String key, final String value, “NX”, “PX”,final long time)这个方法就能同时满足上面的条件,NX:表示存在该key则插入失败,否则插入成功,PX:表示过期,time:表示设置的过期时间,可根据自己的业务设置,key和value就不解释了

(4)加锁的代码实现

public void lock() {
	if (tryLock()){//如果获取锁成功的话,就直接返回h
		System.out.println("线程"+Thread.currentThread().getName()+"获取到锁");
		return;
	}
	try {
		System.out.println("线程"+Thread.currentThread().getName()+"未获得到锁,进行等待...");
		Thread.sleep(10);//没获取到锁,就睡眠,有点影响性能
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	lock();//递归调用
}


@Override
public boolean tryLock() {
	//用于标记所的名字,方便解锁时判断
	String lockName = UUID.randomUUID().toString();
	//如果不存在key就插入,否则插入失败,过期时间为1s
	String ret = jedisUtils.set(KEY, lockName, "NX", "PX", 1000);
	if ("OK".equals(ret)){
		threadLocal.set(lockName);
		return true;
	}
	return false;
}

上面的加锁就实现完成了,jedisUtils.set()是我封装了一个工具类,其实里面就是调用了jedis.set()只是封装了获取jedis的过程,另外使用了Threadlocal变量来维护锁的名字,方便解锁时线程能够获取它加锁的名字(threadlocal相当于一个副本,可跨方法,不熟悉的可以了解一下)

2、解锁

(1)解锁过程要是原子性

(2)谁加的锁,谁去解锁,不能解除它人的锁

先带大家看一段解锁的代码,看看下面代码是否能够同时满足上面两点

/**
* 错误的解锁方式
*/
public void unlockWrong(){
	String lockName = jedisUtils.get(KEY);
	//谁加锁了,就是谁解锁,防止解除他人的锁,但是这种方法不是原子性的,有问题
	if (null != lockName && lockName.equals(threadLocal.get())){
		//如果这个地方锁过期了,然后其他人抢到锁,那么它就是解除掉了他人的锁
		jedisUtils.del(KEY);
	}
}

上面的错误的解锁方式乍一看好像没什么问题,但是仔细看的话,会发现可能会解除掉别人的锁,上面注释已经写的很清楚了。那如何解决呢?要知道如何解决就需要知道上面产生错误的具体原因是什么—解锁的过程不是原子性的,但是并没有向加锁的方式一样给我们提供一个解锁的原子性的方法啊,莫慌,我们可以使用lua脚本—-lua脚本是原子性的

(3)解锁的lua脚本

if redis.call("get",KEYS[1]) == ARGV[1] then 
    return redis.call("del",KEYS[1]) 
else 
    return 0 
end

KEYS[1]:传入的第一个参数

ARGV[1]:传入的第二个参数

上面的意思就是获取某个key对应的value是否和传入的第二个参数相等,如果是则删除,其实就是上面错误解锁方法的内容

(4)解锁的代码

/**
*正确的解锁方式,应当保证原子性,应该使用lua脚本 
 */
@Override
public void unlock() {
	String script = getLuaString("unlock.lua");
	jedisUtils.eval(script, Arrays.asList(KEY),Arrays.asList(threadLocal.get()));
	System.out.println("线程"+Thread.currentThread().getName()+"释放锁成功");
}

上面就是解锁的代码,哦对了,getLuaString()是通过流的方式获取lua脚本的内容,给大家看下代码

private String getLuaString(String luaName){
	String luaPath = this.getClass().getClassLoader().getResource(luaName).getPath();
	//System.out.println("luaPath="+luaPath);
	String ret="";
	try {
		BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File(luaPath))));
		String temp;
		while((temp = reader.readLine()) !=null){
			ret+=temp;
		}
			
	} catch (FileNotFoundException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
	return ret;
}

好了,大概就是上面这些内容了,最后我们来测试一下吧

RedisLockTest.java

package com.taolong;

import java.util.concurrent.CountDownLatch;

import com.taolong.lock.RedisLock;

public class RedisLockTest {

	private static CountDownLatch countDownLatch = new CountDownLatch(10);
	public static void main(String[] args) {
		for (int i=0;i<10;i++){
			new Thread(new LockRunnable()).start();
			countDownLatch.countDown();
		}
	}
	
	private static class LockRunnable implements Runnable{

		@Override
		public void run() {
			RedisLock redisLock = new RedisLock();
			try {
				countDownLatch.await();
				redisLock.lock();
				//模拟操作任务
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				redisLock.unlock();
			}
		}
		
	}
}

运行的结果:

分布式锁----数据库和redis实现分布式锁

 好了今天就分享到这里了,如果有问题,欢迎指正,谢谢!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/111224.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • IDE工具(14) idea创建JavaWeb项目(小白教程)[通俗易懂]

    IDE工具(14) idea创建JavaWeb项目(小白教程)[通俗易懂]创建项目第一步:finish之后,项目结构如下图所示第二步:在WEB-INF下新建classes和lib2个文件夹(Directory)配置项目第三步:配置javaclass字节码编译路径,点击第四步:配置jar包存放路径lib文件夹然后ok就好了classes和lib配置好之后发现颜色会出现变化第五步:配置Tom…

    2022年9月20日
    0
  • micropython 中socket中的非阻塞 报错 Error->: [Errno 119] EINPROGRESS

    micropython 中socket中的非阻塞 报错 Error->: [Errno 119] EINPROGRESS之前的连接方式的如下:sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)addr=socket.getaddrinfo(host,port)[0][-1]sock.setblocking(False)#非阻塞sock.connect(addr)

    2022年7月16日
    12
  • 修改MySQL表名

    修改MySQL表名一、使用SQLALTERTABLE原表名RENAMETO目标表名;二、使用Navicat如果你使用的是Navicat的话就可以直接点击表名后按F2就可以修改表名修改后保存即可…

    2022年5月22日
    30
  • MATLAB切比雪夫带通滤波器

    MATLAB切比雪夫带通滤波器原始信号由5Hz,50Hz,110Hz三种频率的正弦信号构成,并含有直流分量。原始信号为:y=sin(5*2*pi*x)+sin(50*2*pi*x)+sin(110*2*pi*x)+0.5;​图1 原始信号 ​使用通带为[10,100]Hz的切比雪夫滤波器,滤波后的信号时域曲线为:图2 滤波信号​对原始信号和滤波信号作傅里叶变换,观察频谱的变化(左图为原始信号…

    2022年5月4日
    152
  • vue3数据劫持_vue实现keepalive

    vue3数据劫持_vue实现keepalive模仿vue的数据劫持,实现mvvm

    2022年4月22日
    131
  • JavaCV开发详解之3:通用拉流器实现,从流媒体服务器拉流rtsp/rtmp录制成视频文件

    JavaCV开发详解之3:通用拉流器实现,从流媒体服务器拉流rtsp/rtmp录制成视频文件javaCV系列文章:javacv开发详解之1:调用本机摄像头视频javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG、javaCV-openCV)javaCV开发详解之3:收流器实现,录制流媒体服务器的rtsp/rtmp视频文件(基于javaCV-FFMPEG)javaCV开发详解之4:转流器实现(也可作…

    2022年5月8日
    133

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号