分布式锁(数据库、ZK、Redis)拍了拍你

一个分布式锁能够具备上面的几种条件,应该来说是比较好的分布式锁了,但是现实中没有十全十美的锁,对于不同的分布式锁,没有最好,只能说那种场景更加适合。

下面我们详细的聊一聊上面说的三种分布式锁的实现原理,先来看看数据库的分布式锁。

数据库分布式锁

==========================================================================

在数据库的分布式锁的实现中,分为**「悲观锁和乐观锁」「悲观锁的实现依赖于数据库自身的锁机制实现」**。

悲观锁方式实现

==========================================================================

若是要测试数据库的悲观的分布式锁,可以执行下面的sql:select … where … for update (排他锁),注意:where 后面的查询条件要走索引,若是没有走索引,会使用全表扫描,锁全表。

当一个数据库表被加上了排它锁,其它的客户端是不能够再对加锁的数据行加任何的锁,只能等待当前持有锁的释放锁。

全表扫描对于测试就没有太大意义了,where后面的条件是否走索引,要注意自己的索引的使用方式是否正确,并且还取决于**「mysql优化器」**。

排它锁是基于InnoDB存储引擎的,在执行操作的时候,在sql中加入for update,可以给数据行加上排它锁。

在代码的代码的层面上使用connection.commit();,便可以释放锁,但是数据库复杂的加锁和解锁、事务等一系列消耗性能的操作,终归是无法抗高并发。

乐观锁方式实现

==========================================================================

数据库乐观锁的方式实现分布式锁是基于**「版本号控制」的方式实现,类似于「CAS的思想」**,它认为操作的过程并不会存在并发的情况,只有在update version的时候才会去比较。

乐观锁的方式并没有锁的等待,不会因为所等待而消耗资源,下面来测试一下乐观锁的方式实现的分布式锁。

乐观锁的方式实现分布式锁要基于数据库表的方式进行实现,我们认为在数据库表中成功存储该某方法的线程获取到该方法的锁,才能操作该方法。

首先要创建一个表用于储存各个线程操作方法的对应该关系表LOCK:

CREATE TABLE LOCK (

ID int PRIMARY KEY NOT NULL AUTO_INCREMENT,

METHODNAME varchar(64) NOT NULL DEFAULT ‘’,

DESCRIPTION varchar(1024) NOT NULL DEFAULT ‘’,

TIME timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

UNIQUE KEY UNIQUEMETHODNAME (METHODNAME) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

复制代码

该表是存储某个方法的是否已经被锁定的信息,若是被锁定则无法获取到该方法的锁,这里注意的是使用UNIQUE KEY唯一约束,表示该方法布恩那个够被第二个线程同时持有。

当你要获取锁的时候,通过执行下面的sql来尝试获取锁:insert into LOCK(METHODNAME,DESCRIPTION) values (‘getLock’,‘获取锁’) ;来获取锁。

这条sql执行的结果有两种成功和失败,成功说明该方法还没有被某个线程所持有,失败则表明数据库中已经存在该条数据,该方法的锁已经被某个线程所持有。

当你需要释放锁的时候,可以通过执行这条sql:delete from LOCK where METHODNAME=‘getLock’;来释放锁。

总结

=====================================================================

乐观锁实现方式还是存在很多问题的,一个是**「并发性能问题」,再者「不可重入」以及「没有自动失效的功能」「非公平锁」**,只要当前的库表中已经存在该信息,执行插入就会失败。

其实,对于上面的问题基于数据库也可以解决,比如:不可重复,你可以**「增加字段保存当前线程的信息以及可重复的次数」**,只要是再判断是当前线程,可重复的次数就会+1,每次执行释放锁就会-1,直到为0。

「没有失效的功能,可以增加一个字段存储最后的失效时间」,根据这个字段判断当前时间是否大于存储的失效时间,若是大于则表明,该方法的索索已经可以被释放。

「非公平锁可以增加一个中间表的形式,作为一个排队队列」,竞争的线程都会按照时间存储于这个中间表,当要某个线程尝试获取某个方法的锁的时候,检查中间表中是否已经存在等待的队列。

每次都只要获取中间表中最小的时间取获取锁,也能坐待排队等候的效果,所有的问题总是有解决的思路。

上面就是两种基于数据库实现分布式锁的方式,但是,数据库实现分布式锁的方式只作为学习的例子,实际中不会使用它作为实现分布式锁,重要的是学习解决问题的思路和思想。

Redis实现的分布式锁

===============================================================================

很多读者Redis事务有啥用,主要是因为Redis的事务并没有Mysql的事务那么强大,所以一般的公司一般确实是用不到。

事务方式实现

=========================================================================

这里就来说一说Redis事务的一个实际用途,它可以用来实现一个简单的秒杀系统的库存扣减,下面我们就来进行代码的实现。

(1)首先使用线程池初始化5000个客户端。

public static void intitClients() {

ExecutorService threadPool= Executors.newCachedThreadPool();

for (int i = 0; i < 5000; i++) {

threadPool.execute(new Client(i));

}

threadPool.shutdown();

while(true){

if(threadPool.isTerminated()){

break;

}

}

}

复制代码

(2)接着初始化商品的库存数为1000。

public static void initPrductNum() {

Jedis jedis = RedisUtil.getInstance().getJedis();

jedisUtils.set("produce", "1000");// 初始化商品库存数

RedisUtil.returnResource(jedis);// 返还数据库连接

}

}

复制代码

(3)最后是库存扣减的每条线程的处理逻辑。

/**

* 顾客线程

* 

* @author linbingwen

*

*/

class client implements Runnable {

Jedis jedis = null;

String key = "produce"; // 商品数量的主键

String name;



public ClientThread(int num) {

name= "编号=" + num;

}



public void run() {



while (true) {

jedis = RedisUtil.getInstance().getJedis();

try {

jedis.watch(key);

int num= Integer.parseInt(jedis.get(key));// 当前商品个数

if (num> 0) {

Transaction ts= jedis.multi(); // 开始事务

ts.set(key, String.valueOf(num - 1)); // 库存扣减

List result = ts.exec(); // 执行事务

if (result == null || result.isEmpty()) {

System.out.println("抱歉,您抢购失败,请再次重试");

} else {

System.out.println("恭喜您,抢购成功");

break;

}

} else {

System.out.println("抱歉,商品已经卖完");

break;

}

} catch (Exception e) {

e.printStackTrace();

} finally {

jedis.unwatch(); // 解除被监视的key

RedisUtil.returnResource(jedis);

}

}

}

}

复制代码

在代码的实现中有一个重要的点就是**「商品的数据量被watch了」**,当前的客户端只要发现数量被改变就会抢购失败,然后不断的自旋进行抢购。

这个是基于Redis事务实现的简单的秒杀系统,Redis事务中的watch命令有点类似乐观锁的机制,只要发现商品数量被修改,就执行失败。

Redis原生命令实现

==============================================================================

Redis实现分布式锁的第二种方式,可以使用setnx、getset、expire、del这四个命令来实现。

  1. setnx:命令表示如果key不存在,就会执行set命令,若是key已经存在,不会执行任何操作。

  2. getset:将key设置为给定的value值,并返回原来的旧value值,若是key不存在就会返回返回nil 。

  3. expire:设置key生存时间,当当前时间超出了给定的时间,就会自动删除key。

  4. del:删除key,它可以删除多个key,语法如下:DEL key [key …],若是key不存在直接忽略。

下面通过一个代码案例是实现以下这个命令的操作方式:

public void redis(Produce produce) {

long timeout= 10000L; // 超时时间

Long result= RedisUtil.setnx(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout));

if (result!= null && result.intValue() == 1) { // 返回1表示成功获取到锁

RedisUtil.expire(produce.getId(), 10);//有效期为5秒,防止死锁

//执行业务操作

//执行完业务后,释放锁

RedisUtil.del(produce.getId());

} else {

System.println.out(“没有获取到锁”)

}

}

复制代码

在线程A通过setnx方法尝试去获取到produce对象的锁,若是获取成功就会返回1,获取不成功,说明当前对象的锁已经被其它线程锁持有。

获取锁成功后并设置key的生存时间,能够有效的防止出现死锁,最后就是通过del来实现删除key,这样其它的线程就也可以获取到这个对象的锁。

执行的逻辑很简单,但是简单的同时也会出现问题,比如你在执行完setnx成功后设置生存时间不生效,此时服务器宕机,那么key就会一直存在Redis中。

当然解决的办法,你可以在服务器destroy函数里面再次执行:

RedisUtil.del(produce.getId());

复制代码

或者通过**「定时任务检查是否有设置生存时间」**,没有的话都会统一进行设置生存时间。

还有比较好的解决方案就是,在上面的执行逻辑里面,若是没有获取到锁再次进行key的生存时间:

public void redis(Produce produce) {

long timeout= 10000L; // 超时时间

Long result= RedisUtil.setnx(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout));

if (result!= null && result.intValue() == 1) { // 返回1表示成功获取到锁

RedisUtil.expire(produce.getId(), 10);//有效期为10秒,防止死锁

//执行业务操作

//执行完业务后,释放锁

RedisUtil.del(produce.getId());

} else {

String value= RedisUtil.get(produce.getId());

// 存在该key,并且已经超时

if (value!= null && System.currentTimeMillis() > Long.parseLong(value)) {

String result = RedisUtil.getSet(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout));

if (result == null || (result != null && StringUtils.equals(value, result))) {

RedisUtil.expire(produce.getId(), 10);//有效期为10秒,防止死锁

//执行业务操作

//执行完业务后,释放锁

RedisUtil.del(produce.getId());

} else {

System.println(“没有获取到锁”)

}

} else {

System.println(“没有获取到锁”)

}

}

}

复制代码

这里对上面的代码进行了改进,在获取setnx失败的时候,再次重新判断该key的锁时间是否失效或者不存在,并重新设置生存的时间,避免出现死锁的情况。

Redisson实现

=============================================================================

第三种Redis实现分布式锁,可以使用Redisson来实现,它的实现简单,已经帮我们封装好了,屏蔽了底层复杂的实现逻辑。

先来一个Redisson的原理图,后面会对这个原理图进行详细的介绍:

image

我们在实际的项目中要使用它,只需要引入它的依赖,然后执行下面的代码:

RLock lock = redisson.getLock(“lockName”);

lock.locl();

lock.unlock();

复制代码

并且它还支持**「Redis单实例、Redis哨兵、redis cluster、redis master-slave」**等各种部署架构,都给你完美的实现,不用自己再次拧螺丝。

但是,crud的同时还是要学习一下它的底层的实现原理,下面我们来了解下一下,对于一个分布式的锁的框架主要的学习分为下面的5个点:

  1. 加锁机制
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后总结

ActiveMQ+Kafka+RabbitMQ学习笔记PDF

image.png

  • RabbitMQ实战指南

image.png

  • 手写RocketMQ笔记

image.png

  • 手写“Kafka笔记”

image

关于分布式,限流+缓存+缓存,这三大技术(包含:ZooKeeper+Nginx+MongoDB+memcached+Redis+ActiveMQ+Kafka+RabbitMQ)等等。这些相关的面试也好,还有手写以及学习的笔记PDF,都是啃透分布式技术必不可少的宝藏。以上的每一个专题每一个小分类都有相关的介绍,并且小编也已经将其整理成PDF啦
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

ActiveMQ+Kafka+RabbitMQ学习笔记PDF

[外链图片转存中…(img-yVwNHH0g-1712835428796)]

  • RabbitMQ实战指南

[外链图片转存中…(img-KSXEp7A0-1712835428796)]

  • 手写RocketMQ笔记

[外链图片转存中…(img-UshMVQrP-1712835428796)]

  • 手写“Kafka笔记”

[外链图片转存中…(img-hqJMLiPr-1712835428796)]

关于分布式,限流+缓存+缓存,这三大技术(包含:ZooKeeper+Nginx+MongoDB+memcached+Redis+ActiveMQ+Kafka+RabbitMQ)等等。这些相关的面试也好,还有手写以及学习的笔记PDF,都是啃透分布式技术必不可少的宝藏。以上的每一个专题每一个小分类都有相关的介绍,并且小编也已经将其整理成PDF啦
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 16
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值