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秒检查一次,可以不断的延长持有锁的时间。
Redisson中可重入锁的实现是通过incrby lockName来实现,「重入一个计数就会+1,释放一次锁计数就会-1」。
最后,使用完锁后执行del lockName就可以直接**「释放锁」**,这样其它的客户端就可以争抢到该锁了。
这就是分布式锁的开源Redisson框架底层锁机制的实现原理,我们可以在生产中实现该框架实现分布式锁的高效使用。
下面通过一个多窗口抢票的例子代码来实现:
public class SellTicket implements Runnable {
private int ticketNum = 1000;
RLock lock = getLock();
// 获取锁
private RLock getLock() {
Config config = new Config();
config.useSingleServer().setAddress(“redis://localhost:6379”);
Redisson redisson = (Redisson) Redisson.create(config);
RLock lock = redisson.getLock(“keyName”);
return lock;
}
@Override
public void run() {
while (ticketNum>0) {
// 获取锁,并设置超时时间
lock.lock(1, TimeUnit.MINUTES);
try {
if (ticketNum> 0) {
System.out.println(Thread.currentThread().getName() + “出售第 " + ticketNum-- + " 张票”);
}
} finally {
lock.unlock(); // 释放锁
}
}
}
}
复制代码
测试的代码如下:
public class Test { public static void main(String[] args) { SellTicket sellTick= new SellTicket(); // 开启5五条线程,模拟5个窗口 for (int i=1; i<=5; i++) { new Thread(sellTick, "窗口" + i).start(); } } } 复制代码
是不是感觉很简单,因为多线程竞争共享资源的复杂的过程它在底层都帮你实现了,屏蔽了这些复杂的过程,而你也就成为了优秀的API调用者。
上面就是Redis三种方式实现分布式锁的方式,基于Redis的实现方式基本都会选择Redisson的方式进行实现,因为简单命令,不用自己拧螺丝,开箱即用。
============================================================================
ZK实现的分布式锁的原理是基于一个**「临时顺序节点」**实现的,开始的时候,首先会在ZK中创建一个ParentLock持久化节点。
当有client1请求锁的时候,,就会在ParentLock下创建一个临时顺序节点,如下图所示:
并且,该节点是有序的,在ZK的内部会自动维护一个节点的序号,比如:第一个进来的创建的临时顺序节点叫做xxx-000001,那么第二个就叫做xxx-000002,这里的序号是一次递增的。
当client1创建完临时顺序节点后,就会检查ParentLock下面的所有的子节点,会判断自己前面是否还有节点,此时明显是没有的,所以获取锁成功。
当第二个客户端client2进来获取锁的时候,也会执行相同的逻辑,会先在创建一个临时的顺序节点,并且序号是排在第一个节点的后面:
并且第二部也会判断ParnetLock下面的所有的子节点,看自己是否是第一个,明显不是,此时就会加锁失败。
那么此时client2会创建一个对client1的lock1的监听(Watcher),用于监听lock1是否存在,同时client2会进入等待状态:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

知其然不知其所以然,大厂常问面试技术如何复习?
1、热门面试题及答案大全
面试前做足功夫,让你面试成功率提升一截,这里一份热门350道一线互联网常问面试题及答案助你拿offer
2、多线程、高并发、缓存入门到实战项目pdf书籍
3、文中提到面试题答案整理
4、Java核心知识面试宝典
覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
g-JGAhWNG5-1712363714557)]
[外链图片转存中…(img-yg8eHqM6-1712363714557)]
3、文中提到面试题答案整理
[外链图片转存中…(img-ZFtuhdGp-1712363714557)]
4、Java核心知识面试宝典
覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入
[外链图片转存中…(img-2JYF8Fov-1712363714558)]
[外链图片转存中…(img-GKFG4Ltw-1712363714558)]
[外链图片转存中…(img-rmLDKkHZ-1712363714558)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!