一个分布式锁能够具备上面的几种条件,应该来说是比较好的分布式锁了,但是现实中没有十全十美的锁,对于不同的分布式锁,没有最好,只能说那种场景更加适合。
下面我们详细的聊一聊上面说的三种分布式锁的实现原理,先来看看数据库的分布式锁。
==========================================================================
在数据库的分布式锁的实现中,分为**「悲观锁和乐观锁」,「悲观锁的实现依赖于数据库自身的锁机制实现」**。
==========================================================================
若是要测试数据库的悲观的分布式锁,可以执行下面的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的事务并没有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个点:
- 加锁机制
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/ba2e6c32cf65513f0069fc9681e617e4.jpeg)
读者福利
由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴
更多笔记分享
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/ba2e6c32cf65513f0069fc9681e617e4.jpeg)
读者福利
由于篇幅过长,就不展示所有面试题了,感兴趣的小伙伴
[外链图片转存中…(img-iO6KXYA7-1711894441317)]
[外链图片转存中…(img-WApZms9S-1711894441317)]
[外链图片转存中…(img-duNtp4EB-1711894441318)]
更多笔记分享
[外链图片转存中…(img-Icp7D0S5-1711894441318)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!