赞
踩
关于面试刷题也是有方法可言的,建议最好是按照专题来进行,然后由基础到高级,由浅入深来,效果会更好。当然,这些内容我也全部整理在一份pdf文档内,分成了以下几大专题:
这份面试文档当然不止这些内容,实际上像JVM、设计模式、ZK、MQ、数据结构等其他部分的面试内容均有涉及,因为文章篇幅,就不全部在这里阐述了。
作为一名程序员,阶段性的学习是必不可少的,而且需要保持一定的持续性,这次在这个阶段内,我对一些重点的知识点进行了系统的复习,一方面巩固了自己的基础,另一方面也提升了自己的知识广度和深度。
数据库乐观锁的方式实现分布式锁是基于**「版本号控制」的方式实现,类似于「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的事务并没有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实现分布式锁的第二种方式,可以使用setnx、getset、expire、del这四个命令来实现。
setnx:命令表示如果key不存在,就会执行set命令,若是key已经存在,不会执行任何操作。
getset:将key设置为给定的value值,并返回原来的旧value值,若是key不存在就会返回返回nil 。
expire:设置key生存时间,当当前时间超出了给定的时间,就会自动删除key。
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的锁时间是否失效或者不存在,并重新设置生存的时间,避免出现死锁的情况。
=============================================================================
第三种Redis实现分布式锁,可以使用Redisson来实现,它的实现简单,已经帮我们封装好了,屏蔽了底层复杂的实现逻辑。
先来一个Redisson的原理图,后面会对这个原理图进行详细的介绍:
我们在实际的项目中要使用它,只需要引入它的依赖,然后执行下面的代码:
RLock lock = redisson.getLock(“lockName”);
lock.locl();
lock.unlock();
复制代码
并且它还支持**「Redis单实例、Redis哨兵、redis cluster、redis master-slave」**等各种部署架构,都给你完美的实现,不用自己再次拧螺丝。
但是,crud的同时还是要学习一下它的底层的实现原理,下面我们来了解下一下,对于一个分布式的锁的框架主要的学习分为下面的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基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等,且还会持续的更新…可star一下!
283页的Java进阶核心pdf文档
Java部分:Java基础,集合,并发,多线程,JVM,设计模式
数据结构算法:Java算法,数据结构
开源框架部分:Spring,MyBatis,MVC,netty,tomcat
分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等
微服务部分:SpringBoot,SpringCloud,Dubbo,Docker
还有源码相关的阅读学习
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)]
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。