一窥:分布式、分布式缓存、分布式锁及其延申

1.分布式

1.集中式

传统的计算模型通常是集中式的,所有的计算任务和数据处理都由单一的计算机或服务器完成。然而,随着数据量和计算需求的增加,集中式系统可能会面临性能瓶颈和可靠性问题。

故而引出了分布式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

2.分布式

分布式是一种利用多台计算机协同工作来完成任务的计算模型

分布式系统的目标是通过将任务和数据分割成小块,将计算任务分发到不同的计算节点上,充分利用多台计算机的处理能力,以应对大规模的计算需求,以提高性能、可扩展性和容错性。

分布式系统的一些问题:数据一致性、通信延迟、故障处理等。为了解决这些问题,需要使用各种技术和算法,如分布式数据库、消息队列、负载均衡、分布式存储等。

2.分布式缓存

例如:redis缓存

分布式缓存是用于存储和管理数据以提高性能和响应速度的一种技术。

通常用于加速应用程序的数据访问,减轻后端数据库或其他数据存储系统的负载。

在分布式缓存中,数据被存储在位于应用程序和数据存储系统之间的一个或多个缓存节点中。这些节点可以位于不同的服务器或计算机上,形成一个分布式的缓存网络。当应用程序需要访问数据时,它首先会查询缓存,如果缓存中存在数据,就可以快速获取而不必访问原始数据存储系统

需要注意的问题:

  1. 缓存穿透

  2. 缓存击穿

  3. 缓存雪崩

  4. 数据一致性

1.缓存穿透

指查询一个数据库中一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录

解决方案:

1.缓存null

当数据库中查不到数据的时候,缓存一个空对象,然后给这个空对象的缓存设置一个过期时间,这样下次再查询该数据的时候,就可以直接从缓存中拿到,从而达到了减小数据库压力的目的

存在缺点:

  1. 需要缓存层提供更多的内存空间来缓存这些空对象,当这种空对象很多的时候,就会浪费更多的内存;

  2. 会导致缓存层和存储层的数据不一致,即使在缓存空对象时给它设置了一个很短的过期时间,那也会导致这一段时间内的数据不一致问题。

2.使用布隆过滤器

布隆过滤器:

布隆过滤器,就是一种数据结构,它是由一个长度为m bit的位数组与n个hash函数组成的数据结构,位数组中每个元素的初始值都是0。在初始化布隆过滤器时,会先将所有key进行n次hash运算,这样就可以得到n个位置,然后将这n个位置上的元素改为1。这样,就相当于把所有的key保存到了布隆过滤器中了。

布隆过滤器就相当于一个位于客户端与缓存层中间的拦截器一样,负责判断key是否在集合中存在

存在缺点:

有误判的可能(位数组长度越大,误判率越低): 例如:

有三个key,经hash运算后

key1: 1 、2 、4

key2: 2 、5 、6

key3: 4、 5 、6 由于4、5、6都以存在已置为1,布隆过滤器就认为key3在库中存在

2.缓存击穿

对于设置了过期时间的key(一个key),缓存在某个时间点过期了,在该热点数据重新载入缓存之前,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。

解决方案:

1.永远不过期
  1. 不对这个key设置过期时间

  2. 正常给key设置过期时间,同时在后台同时启一个定时任务去定时地更新这个缓存

2.使用分布式锁

同一时刻只能有一个查询请求重新加载热点数据到缓存中。

这样,其他的线程只需等待该线程运行完毕,即可重新从Redis中获取数据

整体的思想:获取锁的时候向Redis中插入数据,释放锁的时候从Redis中删除数据。

使用的命令: setnx

详细往下看↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

3.缓存雪崩

当缓存中有大量的key在同一时刻过期,或者Redis直接宕机了,导致大量的查询请求全部到达数据库,造成数据库查询压力骤增,甚至直接挂掉

解决方案:

1.添加过期随机数,使key均匀失效

将key的过期时间后面加上一个随机数,这个随机数值的范围可以根据自己的业务情况自行设定,这样可以让key均匀的失效,避免大批量的同时失效。

4.数据一致性问题

因为数据库与缓存是不同的组件,操作必须有先后顺序,无法像数据库的事务一样满足ACID的特性,所以就会出现数据在缓存中与在数据

库中不一致的问题。

在更新的时候,操作缓存和数据库无疑就是以下四种可能之一:

1.双写模式:

  • 先更新缓存,再更新数据库

  • 先更新数据库,再更新缓存

2.失效模式:

  • 先删除缓存,再更新数据库

  • 先更新数据库,再删除缓存

1.先更新缓存,在更新数据库

如果我成功更新了缓存,但是在执行更新数据库的那一步,服务器突然宕机了,那么此时,我的缓存中是最新的数据,而数据库中是旧的数据。

2.先更新数据库,在更新缓存

如果我成功更新了数据库,但是在执行更新缓存的那一步,服务器突然宕机了,那么此时,我的缓存中就是老数据,而数据库中是新的数据。

3.先删除缓存,在更新数据库
非高并发情况下可保证数据一致性:

若删除缓存成功,更新数据库失败,下次操作会去数据库中获取该数据,并将最新数据写入缓存

读写发情况下会出现数据不一致问题:

4.先更新数据库,在删除缓存
1.更新数据库成功了,而删除缓存失败

数据库中就会是新数据,而缓存中是旧数据,数据就出现了不一致情况:

2.更新数据库成功了,删除缓存也执行成功

还是会造成数据的不一致性

但是此处达成这个数据不一致性的条件明显会比起其他的方式更为困难

  • 时刻1:读请求的时候,缓存正好过期

  • 时刻2:读请求在写请求更新数据库之前查询数据库,

  • 时刻3:写请求,在更新数据库之后,要在读请求成功写入缓存前,先执行删除缓存操作。

这通常是很难做到的,因为在真正的并发开发中,更新数据库是需要加锁的,不然没一点安全性

5.延迟双删

先进行缓存清除,在更新数据库,最后(延迟N秒确保数据库已更新)再执行缓存清除。进行两次删除,且中间需要延迟一段时间

假设有一个电商网站,用户可以通过搜索商品名称来查找商品信息。为了提高搜索速度,系统会将商品信息缓存在内存中。当用户进行搜索时,系统首先检查缓存中是否存在该商品的信息,如果存在则直接返回结果,否则从数据库中查询并更新缓存。

现在,假设某个商品的库存发生了变化,需要更新数据库中的库存信息。此时,系统可以采用延迟双删策略来确保数据的一致性。具体步骤如下:

  1. 删除缓存中的该商品信息。
  2. 更新数据库中的库存信息。
  3. 再次检查数据库中的库存信息是否已经更新成功。
  4. 如果更新成功,则再次删除缓存中的该商品信息。

通过这样的处理方式,可以确保在下一次用户搜索该商品时,能够获取到最新的库存信息,避免了数据不一致的问题。同时,由于只删除了缓存中的特定商品信息,而不是全部缓存,因此对系统性能的影响较小。

上述方案在极端情况下,如果第三步删除失败仍然可能导致数据一致性问题,解决方案有:

⭐⭐⭐⭐⭐引入MQ重试机制,基于数据库binlog的方式增量解析、订阅和消费

为了保证删除成功,可以利用阿里巴巴开源中间件canal订阅binlog发送到MQ中,再利用MQ的ACK机制来保证删除成功,最终保证数据缓存一致性

我们通过一个简化的例子来说明如何在更新数据库时使用延迟双删策略,并结合Canal和MQ的ACK机制以确保数据一致性:

  1. 初始状态

    • 数据库中的商品库存为10。
    • 缓存中的商品库存为10(假设缓存已经预热)。
  2. 第一步:删除缓存

    • 当商品库存需要更新时(比如减库存),首先删除缓存中的商品库存信息。
  3. 第二步:更新数据库

    • 应用程序更新数据库中的商品库存,将库存减少到9。
  4. 第三步:等待Canal抓取binlog

    • Canal监控MySQL的binlog,捕获到商品库存更新的事件。
  5. 第四步:发送到MQ

    • Canal将解析后的库存更新事件发送到消息队列(MQ)。
  6. 第五步:消费者接收MQ消息

    • 消费者从MQ中获取到Canal发送的库存更新事件。
  7. 第六步:消费者处理MQ消息

    • 消费者根据MQ消息中的事件内容,更新本地缓存中的商品库存为9。
  8. 第七步:发送ACK给MQ

    • 消费者完成缓存更新后,向MQ发送ACK确认消息,告知MQ该事件已经被成功处理。
  9. 第八步:延迟双删

    • 在发送ACK之前,消费者执行一次延迟操作(例如等待几毫秒至一秒),以确保即便是在高并发情况下,Canal有足够的时间将binlog事件发送到MQ,并且其他所有依赖此数据的消费者节点也有足够的时间来处理这个事件。
  10. 第九步:确认数据库修改

    • 经过短暂的延迟后,消费者确认数据库中的库存确实已更新为9。
  11. 第十步:再次删除缓存(如果需要)

    • 如果有必要,消费者可以再次检查缓存中的数据,并进行删除或更新。
  12. 第十一步:再次发送ACK给MQ(如果需要)

    • 如果进行了第十步的操作,消费者可以再次向MQ发送ACK确认消息,以表明缓存中的数据也已经被成功更新。

3.分布式锁

jdk的锁: 1、显示锁:Lock 2、隐式锁:synchronized

使用jdk锁保证线程的安全性要求:要求多个线程必须运行在同一个jvm中

但现在的系统基本都是分布式部署的,一个应用会被部署到多台服务器上,synchronized只能控制当前服务器自身的线程安全,并不能跨服务器控制并发安全。

所以在分布式环境下要解决线程安全问题就需要使用分布式锁

思想:需要在我们分布式应用的外面使用一个第三方组件(可以是数据库、Redis、Zookeeper等)进行全局锁的监控,由这个组件决定什么时候加锁,什么时候释放锁

原理:在获取锁的时候插入数据,如何数据可以存储成功那么就获取获取到了锁,如果数据插入不成功那么就说明获取锁失败了。在进行锁释放的时候只需要将数据删除掉。

1.🌟redis分布式锁实现业务流程:
  1. 首先我们项目中是基于原生redis实现分布式的就会涉及到一些redis原生命令

  2. 前置操作一定是缓存无数据,布隆判断之后可能有数据,才会在此处添动加商品自身分布式锁

  3. 使用set nx ex命令设置一个有效期为指定时间的锁,我记得当时是根据多次压测结果取的值

  4. 这样会有一个问题,万一在给定时间内未完成查询操作此时我们是通过后端代码自定义守护线程方式位锁进行自动续期

  5. 上面的操作如果都没有问题,表示上锁成功.回源查询数据库写入缓存,整个分布式锁的实现会涉及到锁的获取、判断、删除,需要保证这三个操作原子性,我们借助于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提供的一个面向切面编程思想,其底层原理是动态代理,项目中是这样做的

  1. 自定了一个注解@MyCache包含redis使用的key的前缀、过期时间、分布式锁key值等信息

  2. 自定义一个切面类,就是一个被@Aspect注解修饰的一个普通类而己,在类中定义一个通知,其实就是方法名around在这个方法上需要加@Around注解表示,我们用的是spring5通知类型中的环绕通知,通过该注解的一个属性annotation对自定义注解进行增强

  3. 鉴于环绕通知的使用方法是固定的,所以在定义环绕通知的时候,需要注意方法返回值必须是Object类型,方法形参必须是ProceedingJoinPoint的,为了能够手动调用目标方法,另外还需要注意,环绕通知方法必须手动抛出异常信息

  4. 这样就完成了项目中对于A0P封装和使用,在需要用到缓存和分布式锁的场景,我们只需要将注解添加到使用的位置即可

3 如何提高分布式锁性能
1 优化分布式锁性能的关键因素

要提升分布式锁的性能,首先需要了解影响性能的关键因素。以下是一些影响分布式锁性能的关键因素:

  1. 锁的粒度:锁的粒度越小,性能通常越高。粒度较大的锁可能会导致锁争用,从而降低性能。

  2. 锁的持有时间:锁的持有时间越短,性能越高。长时间持有锁会限制其他节点的访问。

  3. 锁的实现方式:不同的分布式锁实现方式性能差异较大。使用缓存的速度比较快。

  4. 网络延迟:分布式锁通常需要跨越网络进行通信,网络延迟会影响性能。

  5. 锁的竞争情况:如果锁的竞争情况较少,性能通常较好。高度竞争的锁会导致性能下降。

2 优化技巧和最佳实践
1. 选择合适的分布式锁实现

选择合适的分布式锁实现是性能优化的关键。不同的实现方式有不同的性能特点。例如,基于

Redis的分布式锁通常性能较高,因为Redis是一个高性能的内存数据库,而基于ZooKeeper的锁可

能性能较低,因为它需要跨越网络进行通信。因此,根据需求选择合适的实现方式非常重要。

2. 减小锁的粒度

将锁的粒度尽量减小可以提高性能。例如,如果系统中有多个共享资源,可以为每个资源使用单独

的锁,而不是一个全局锁。这样可以减小锁的竞争情况,提高吞

吐量。

3. 限制锁的持有时间

尽量减小锁的持有时间可以提高性能。在获取锁后,尽快完成需要锁保护的操作,然后释放锁,让

其他节点有机会访问共享资源。

4. 使用非阻塞锁

非阻塞锁通常性能更高,因为它们不会阻塞线程或进程,而是会立即返回锁的状态。常见的非阻塞

锁包括乐观锁和基于CAS(比较并交换)的锁。

5. 考虑锁的超时和重试机制

在获取锁时,考虑设置锁的超时时间和重试机制,以避免出现死锁情况。如果获取锁失败,可以等

待一段时间后重试,或者使用指数退避策略。

6. 考虑分布式事务

在某些场景下,使用分布式事务可以代替分布式锁,从而提高性能。分布式事务通常比分布式锁更

高效,但需要谨慎设计,以确保数据一致性。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NIIMP

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值