1.分布式锁
jdk的锁: 1、显示锁:Lock 2、隐式锁:synchronized
使用jdk锁保证线程的安全性要求:要求多个线程必须运行在同一个jvm中
但现在的系统基本都是分布式部署的,一个应用会被部署到多台服务器上,synchronized只能控制当前服务器自身的线程安全,并不能跨服务器控制并发安全。
所以在分布式环境下要解决线程安全问题就需要使用分布式锁
思想:需要在我们分布式应用的外面使用一个第三方组件(可以是数据库、Redis、Zookeeper等)进行全局锁的监控,由这个组件决定什么时候加锁,什么时候释放锁
原理:在获取锁的时候插入数据,如何数据可以存储成功那么就获取获取到了锁,如果数据插入不成功那么就说明获取锁失败了。在进行锁释放的时候只需要将数据删除掉。
1.🌟redis分布式锁实现业务流程:
-
首先我们项目中是基于原生redis实现分布式的就会涉及到一些redis原生命令
-
前置操作一定是缓存无数据,布隆判断之后可能有数据,才会在此处添动加商品自身分布式锁
-
使用set nx ex命令设置一个有效期为指定时间的锁,我记得当时是根据多次压测结果取的值
-
这样会有一个问题,万一在给定时间内未完成查询操作此时我们是通过后端代码自定义守护线程方式位锁进行自动续期
-
上面的操作如果都没有问题,表示上锁成功.回源查询数据库写入缓存,整个分布式锁的实现会涉及到锁的获取、判断、删除,需要保证这三个操作原子性,我们借助于lu脚本实现这个一般也不需要记用的时候,通过文档查阅即可
1.基础实现
基于setnx命令的特性,我们就可以实现一个最简单的分布式锁了。我们通过向Redis发送 setnx 命令,然后判断Redis返回的结果是否为1,结果是1就表示setnx成功了,那本次就获得锁了,可以继续执行业务逻辑;如果结果是0,则表示setnx失败了,那本次就没有获取到锁,可以通过循环的方式一直尝试获取锁,直至其他客户端释放了锁(delete掉key)后,就可以正常执行setnx命令获取到锁。流程如下:
2.lua原子性操作
针对上述Redis原始命令无法满足部分业务原子性操作的问题,Redis提供了Lua脚本的支持。
释放锁时:
Lua脚本是一种轻量小巧的脚本语言,它支持原子性操作,Redis将整个Lua脚本作为一个整体执行,中间不会被其他请求插入,因此Redis执行Lua脚本是一个原子操作。在上面的流程中,我们把get key value、判断value是否属于当前线程、删除锁这三步写到Lua脚本中,使它们变成一个整体交个Redis执行,改造后流程如下:
解决了释放锁时取值、判断值、删除锁等多个步骤无法保证原子操作的问题了
加锁时:
在使用set key value ex seconds nx命令加锁时,并不能做到重入锁的效果,也就是当一个线程获取到锁后,在没有释放这把锁之前,当前线程自己也无法再获得这把锁,这显然会影响系统的性能。使用Lua脚本就可以解决这个问题,我们可以在Lua脚本中先判断锁(key)是否存在,如果存在则再判断持有这把锁的线程是否是当前线程,如果不是则加锁失败,否则当前线程再次持有这把锁,并把锁的重入次数+1。在释放锁时,也是先判断持有锁的线程是否是当前线程,如果是则将锁的重入次数-1,直至重入次数减至0,即可删除该锁(key)。
2.🌟Aop-分布式锁(首页三级分类、详情页)
分布式锁实现、A0P在项目中的使用场景
本身咱们在不使用缓存和分布式锁的情况下,也可以实现详情页或者首页三级分类信息的展示,使用了缓存和分布式锁,只是对核心功能的一个增强,按照00P思想,会直接侵入代码不易维护,所以需要将这种从上到下的关系优化为从左到右的增强,即AOP思想AOP是spring提供的一个面向切面编程思想,其底层原理是动态代理,项目中是这样做的
-
自定了一个注解@MyCache包含redis使用的key的前缀、过期时间、分布式锁key值等信息
-
自定义一个切面类,就是一个被@Aspect注解修饰的一个普通类而己,在类中定义一个通知,其实就是方法名around在这个方法上需要加@Around注解表示,我们用的是spring5通知类型中的环绕通知,通过该注解的一个属性annotation对自定义注解进行增强
-
鉴于环绕通知的使用方法是固定的,所以在定义环绕通知的时候,需要注意方法返回值必须是Object类型,方法形参必须是ProceedingJoinPoint的,为了能够手动调用目标方法,另外还需要注意,环绕通知方法必须手动抛出异常信息
-
这样就完成了项目中对于A0P封装和使用,在需要用到缓存和分布式锁的场景,我们只需要将注解添加到使用的位置即可
3 如何提高分布式锁性能
1 优化分布式锁性能的关键因素
要提升分布式锁的性能,首先需要了解影响性能的关键因素。以下是一些影响分布式锁性能的关键因素:
-
锁的粒度:锁的粒度越小,性能通常越高。粒度较大的锁可能会导致锁争用,从而降低性能。
-
锁的持有时间:锁的持有时间越短,性能越高。长时间持有锁会限制其他节点的访问。
-
锁的实现方式:不同的分布式锁实现方式性能差异较大。使用缓存的速度比较快。
-
网络延迟:分布式锁通常需要跨越网络进行通信,网络延迟会影响性能。
-
锁的竞争情况:如果锁的竞争情况较少,性能通常较好。高度竞争的锁会导致性能下降。
2 优化技巧和最佳实践
1. 选择合适的分布式锁实现
选择合适的分布式锁实现是性能优化的关键。不同的实现方式有不同的性能特点。例如,基于
Redis的分布式锁通常性能较高,因为Redis是一个高性能的内存数据库,而基于ZooKeeper的锁可
能性能较低,因为它需要跨越网络进行通信。因此,根据需求选择合适的实现方式非常重要。
2. 减小锁的粒度
将锁的粒度尽量减小可以提高性能。例如,如果系统中有多个共享资源,可以为每个资源使用单独
的锁,而不是一个全局锁。这样可以减小锁的竞争情况,提高吞
吐量。
3. 限制锁的持有时间
尽量减小锁的持有时间可以提高性能。在获取锁后,尽快完成需要锁保护的操作,然后释放锁,让
其他节点有机会访问共享资源。
4. 使用非阻塞锁
非阻塞锁通常性能更高,因为它们不会阻塞线程或进程,而是会立即返回锁的状态。常见的非阻塞
锁包括乐观锁和基于CAS(比较并交换)的锁。
5. 考虑锁的超时和重试机制
在获取锁时,考虑设置锁的超时时间和重试机制,以避免出现死锁情况。如果获取锁失败,可以等
待一段时间后重试,或者使用指数退避策略。
6. 考虑分布式事务
在某些场景下,使用分布式事务可以代替分布式锁,从而提高性能。分布式事务通常比分布式锁更
高效,但需要谨慎设计,以确保数据一致性。