当前位置:   article > 正文

最新分布式锁(数据库、ZK、Redis)拍了拍你(2),GitHub标星1w的Java架构师必备技能

最新分布式锁(数据库、ZK、Redis)拍了拍你(2),GitHub标星1w的Java架构师必备技能

最后

关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:

  • Java基础部分

  • 算法与编程

  • 数据库部分

  • 流行的框架与新技术(Spring+SpringCloud+SpringCloudAlibaba)

这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。

作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

数据库乐观锁的方式实现分布式锁是基于**「版本号控制」的方式实现,类似于「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. 加锁机制

  2. 解锁机制

  3. 生存时间延长机制

  4. 可重入加锁机制

  5. 锁释放机制

只要掌握一个框架的这五个大点,基本这个框架的核心思想就已经掌握了,若是要你去实现一个锁机制框架,就会有大体的一个思路。

Redisson中的加锁机制是通过lua脚本进行实现,Redisson首先会通过**「hash算法」**,选择redis cluster集群中的一个节点,接着会把一个lua脚本发送到Redis中。

它底层实现的lua脚本如下:

returncommandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,

"if (redis.call(‘exists’, KEYS[1]) == 0) then " +

"redis.call(‘hset’, KEYS[1], ARGV[2], 1); " +

"redis.call(‘pexpire’, KEYS[1], ARGV[1]); " +

"return nil; " +

"end; " +

"if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then " +

"redis.call(‘hincrby’, KEYS[1], ARGV[2], 1); " +

"redis.call(‘pexpire’, KEYS[1], ARGV[1]); " +

"return nil; " +

"end; " +

“return redis.call(‘pttl’, KEYS[1]);”,

Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

我的面试宝典:一线互联网大厂Java核心面试题库

以下是我个人的一些做法,希望可以给各位提供一些帮助:

整理了很长一段时间,拿来复习面试刷题非常合适,其中包括了Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等,且还会持续的更新…可star一下!

image

283页的Java进阶核心pdf文档

Java部分:Java基础,集合,并发,多线程,JVM,设计模式

数据结构算法:Java算法,数据结构

开源框架部分:Spring,MyBatis,MVC,netty,tomcat

分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等

微服务部分:SpringBoot,SpringCloud,Dubbo,Docker

image

还有源码相关的阅读学习

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

ubDNvNZs-1715663420624)]

283页的Java进阶核心pdf文档

Java部分:Java基础,集合,并发,多线程,JVM,设计模式

数据结构算法:Java算法,数据结构

开源框架部分:Spring,MyBatis,MVC,netty,tomcat

分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等

微服务部分:SpringBoot,SpringCloud,Dubbo,Docker

[外链图片转存中…(img-fuaN7gTH-1715663420625)]

还有源码相关的阅读学习

[外链图片转存中…(img-n3g3Xo1p-1715663420625)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/707447
推荐阅读
相关标签
  

闽ICP备14008679号