最后
由于篇幅限制,小编在此截出几张知识讲解的图解
该表是存储某个方法的是否已经被锁定的信息,若是被锁定则无法获取到该方法的锁,这里注意的是使用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));
复制代码
「redis.call()的第一个参数表示要执行的命令,KEYS[1]表示要加锁的key值,ARGV[1]表示key的生存时间,默认时30秒,ARGV[2]表示加锁的客户端的ID。」
比如第一行中redis.call(‘exists’, KEYS[1]) == 0) 表示执行exists命令判断Redis中是否含有KEYS[1],这个还是比较好理解的。
lua脚本中封装了要执行的业务逻辑代码,它能够保证执行业务代码的原子性,它通过hset lockName命令完成加锁。
若是第一个客户端已经通过hset命令成功加锁,当第二个客户端继续执行lua脚本时,会发现锁已经被占用,就会通过pttl myLock返回第一个客户端的持锁生存时间。
若是还有生存时间,表示第一个客户端会继续持有锁,那么第二个客户端就会不停的自旋尝试去获取锁。
假如第一个客户端持有锁的时间快到期了,想继续持有锁,可以给它启动一个watch dog看门狗,他是一个后台线程会每隔10秒检查一次,可以不断的延长持有锁的时间。
写在最后
可能有人会问我为什么愿意去花时间帮助大家实现求职梦想,因为我一直坚信时间是可以复制的。我牺牲了自己的大概十个小时写了这片文章,换来的是成千上万的求职者节约几天甚至几周时间浪费在无用的资源上。
上面的这些(算法与数据结构)+(Java多线程学习手册)+(计算机网络顶级教程)等学习资源
锁。
假如第一个客户端持有锁的时间快到期了,想继续持有锁,可以给它启动一个watch dog看门狗,他是一个后台线程会每隔10秒检查一次,可以不断的延长持有锁的时间。
写在最后
可能有人会问我为什么愿意去花时间帮助大家实现求职梦想,因为我一直坚信时间是可以复制的。我牺牲了自己的大概十个小时写了这片文章,换来的是成千上万的求职者节约几天甚至几周时间浪费在无用的资源上。
[外链图片转存中…(img-KjbL4Ha1-1715595672688)]
[外链图片转存中…(img-RbcvTV4U-1715595672689)]
上面的这些(算法与数据结构)+(Java多线程学习手册)+(计算机网络顶级教程)等学习资源