上海龙华医院的网站建设,wordpress 常规选项,网页设计代码步骤,佛山网络推广公司前言
在高并发分布式下#xff0c;我们往往采用分布式锁去维护一个同步互斥的业务需求#xff0c;但是大家细想一下#xff0c;在一些高TPS的业务场景下#xff0c;让这些请求全部卡在获取分布式锁#xff0c;这会造成什么问题#xff1f;
瞬时高并发压垮系统
众所周知…前言
在高并发分布式下我们往往采用分布式锁去维护一个同步互斥的业务需求但是大家细想一下在一些高TPS的业务场景下让这些请求全部卡在获取分布式锁这会造成什么问题
瞬时高并发压垮系统
众所周知一个 SpringBoot 应用的同一时间在运行的请求是有限的因为 SpringBoot 处理请求底层也是个线程池。我截图个 Hippo4j 监控到的 SpringBoot Tomcat 容器线程池举例。 通过上图得知SpringBoot Tomcat 容器默认情况下同一时间最多能处理 200 个请求。如果要应对上千万的 TPS 明显是不可能的。
如果我们直接上分布式锁来维护那么一个同步互斥的业务需求大量请求会因为分布式锁的申请而发生阻塞导致请求无法快速处理。这会导致后续请求长时间被阻塞使系统陷入假死状态。无论请求的数量有多大系统都无法返回响应。此外随着请求的积累还存在内存溢出的风险。更糟糕的是如果 SpringBoot Tomcat 的线程池被分布式锁占用查询请求也将无法得到响应。系统直接嘎了....
无效请求浪费资源
假如一趟列车有几十万人抢票但是真正能购票的用户可能也就几千人。也就意味着哪怕几十万人都去请求这个分布式锁最终也就几十万人中的几千人是有效的其它都是无效获取分布式锁的行为。这样子就给 Redis 申请分布式锁带来巨大的开销压力
那么针对上述两个问题我们该如何优雅解决且看下文解析
资源限流算法
1. 什么是限流
限流Rate Limiting是一种应用程序或系统资源管理的策略用于控制对某个服务、接口或功能的访问速率。它的主要目的是防止过度的请求或流量超过系统的处理能力从而保护系统的稳定性、可靠性和安全性。
通过限制访问速率限流可以防止以下问题的发生
过度使用资源限流可以防止某个用户或客户端过度使用系统资源从而保护服务器免受过载的影响。防止垃圾请求限流可以过滤掉恶意或无效的请求例如恶意攻击、爬虫或垃圾邮件发送等。维护服务质量通过限制访问速率可以确保每个请求都能够得到适当的处理和响应时间从而提高服务质量和用户体验。控制成本限流可以帮助控制系统资源的使用避免因为过多的请求而导致不必要的成本增加。
上文也讲到过一个列车可能就几百上千人能购买成功但可能会有远超过这个量级的用户进行抢票在真正执行抢票逻辑前可以通过限流算法进行限制只让少量用户操作购票流程。
2. 常见限流算法
限流可以通过多种算法方式实现比如
固定窗口算法Fixed Window Algorithm该算法将时间划分为固定大小的窗口例如每秒、每分钟或每小时。在每个窗口内限制请求的数量不得超过预设的阈值。这种算法简单直观但可能存在突发请求超过阈值的问题。滑动窗口算法Sliding Window Algorithm该算法将时间划分为连续的时间片段例如每秒划分为多个小的时间片段。每个时间片段都有自己的请求计数器记录在该时间片段内的请求数量。当请求到达时会逐渐删除过时的时间片段并根据当前时间片段内的请求数量判断是否超过阈值。这种算法可以更好地处理突发请求。令牌桶算法Token Bucket Algorithm该算法模拟了一个令牌桶桶中以固定速率生成令牌。每个令牌代表一个请求的许可。当请求到达时需要先从令牌桶中获取令牌如果桶中没有足够的令牌则请求被限制。这种算法可以平滑地控制请求的速率。漏桶算法Leaky Bucket Algorithm该算法类似于一个漏桶请求以固定速率进入漏桶。如果漏桶已满则多余的请求将被丢弃或延迟处理。这种算法可以稳定请求的处理速率防止突发请求对系统造成压力。
这些算法都有不同的特点和适用场景选择适合的限流算法取决于应用程序的需求和预期的限流效果。在实际应用中也可以根据具体情况结合多种算法来实现更复杂的限流策略。
这些算法网上介绍的较为完善大家可以搜索相关文章详细了解这里不过多赘述。
实际业务学习 假设我们现在需要设计一个架构来满足国庆假期热门列车的车票售卖业务 业务分析
对于五一、国庆以及过年这些节日来说一些热门列车的 TPS 少说有几十万 TPS。如果仅仅采用所有请求都进行分布式锁竞争去同步互斥进行购座下单的设计直接就会导致前面提到的瞬时高并发压垮系统问题那这块的分布式锁逻辑是不是可以优化呢比如不让所有抢购列车的用户去申请分布式锁而是让少量用户去请求获取分布式锁。这样优化的话可以极大情况节省 Redis 申请分布式锁的开销压力。
优化思路
我们可以采用双重判定锁的思路在竞争分布式锁前判断它有没有资格去竞争先只要没有资格竞争的就一边凉快儿去只有剩下那些具备竞争资格的请求才能到达下一步竞争环境大家想想对于当前业务场景来说如果把车票当作一个令牌在竞争锁前先让他们去抢这些令牌只有抢到令牌的人才能进行竞争分布式锁同步互斥下单操作那么你看几十万的TPS不就变成了几千个TPS了嘛这样优化的话可以极大情况节省 Redis 申请分布式锁的开销压力。
伪代码实现
相信大家已经明白了精髓这里我就不贴多详细的代码了精华往往一点即通~以下是简要的伪代码
if令牌容器在缓存中失效{重新读取令牌资源并放入缓存中充当令牌容器
}
String token Lua脚本实现查询余额大于0就返回并余额减一确保两操作的原子性
if(token ! null){RLock lock redissonClient.getFairLock(lockKey);lock.lock();try {// 执行购票流程return executePurchaseTickets(requestParam);} finally {// 释放分布式公平锁lock.unlock();}
}不知道上述讲解大家对于分布式锁的运用设计有没有新的思路呢但是还没有结束噢下面我们再来深入一下 本地分布式多重锁
优化思路
类似于这种有加分布式锁逻辑的大多数都是集群化部署是否需要考虑封装下加锁逻辑呢比如线程先去竞争单个服务的内部锁竞争成功再去竞争分布式锁从而减少redis的压力其实本质上就是一个逐级打怪的过程我先在蛇窝里当上蛇头了代表所有蛇去龙穴里去和其他的蛇头竞争龙头那么经过这么一轮的再度过滤竞争的分布式锁的TPS是不是就更小了呢?
1. 构建本地分布式多重锁
接口的实现逻辑需要再次重构下从单分布式锁的获取变为多种锁的组合获取。
private final ConcurrentHashMapString, ReentrantLock localLockMap new ConcurrentHashMap();Override
Transactional(rollbackFor Throwable.class)
public TicketPurchaseRespDTO purchaseTicketsV2(PurchaseTicketReqDTO requestParam) {// .....// 构建锁唯一 KeyString lockKey environment.resolvePlaceholders(String.format(LOCK_PURCHASE_TICKETS, requestParam.getTrainId()));// 根据锁唯一 Key 获取本地锁通过 ConcurrentHashMap 保证并发读写数据安全ReentrantLock localLock localLockMap.computeIfAbsent(lockKey, key - new ReentrantLock(true));// 先获取本地公平锁因为咱们上面创建锁指定了公平模式 new ReentrantLock(true)localLock.lock();try {// 获取到本地公平锁后开始获取分布式公平锁RLock lock redissonClient.getFairLock(lockKey);lock.lock();try {// 执行购票流程return executePurchaseTickets(requestParam);} finally {// 释放分布式公平锁lock.unlock();}} finally {// 释放本地公平锁localLock.unlock();}
}从实现咱们上述功能来说这个代码已经没问题了。但是仔细思考下是否还有一些潜在逻辑是没考虑到的
2. 本地锁内存安全思考
上面这个程序安全么在看到这里时大家思考下。
结论是不安全的可能会有内存溢出的风险。问题就出在本地锁存储容器上。
我们通过 ConcurrentHashMap 存储每个列车的本地锁作为申请分布式锁之前的一层性能挡板隔绝无效流量请求 Redis。但是大家发现没有这个 ConcurrentHashMap 是只能存储但是没有任何过期策略。这样会导致一个问题就是应用长时间不发布越来越多的列车数据存储在容器中直到内存溢出为止。
怎么实现一个线程安全以及内存安全的本地锁容器伪代码如下大家仅作为参考即可。
通过 Caffeine 创建本地安全锁容器Caffeine 的 expireAfterWrite 方法代表放入元素过期的时间是什么。比如咱们以下案例中配置的一天过期代表一个列车的本地公平锁创建一天后失效。
private final CacheString, ReentrantLock localLockMap Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.DAYS).build();Override
Transactional(rollbackFor Throwable.class)
public TicketPurchaseRespDTO purchaseTicketsV2(PurchaseTicketReqDTO requestParam) {// ......String lockKey environment.resolvePlaceholders(String.format(LOCK_PURCHASE_TICKETS, requestParam.getTrainId()));// getIfPresent 类似于 HashMap 中 get 方法ReentrantLock localLock localLockMap.getIfPresent(lockKey);// 不存在的话执行加载流程if (localLock null) {// Caffeine 不像 ConcurrentHashMap 做了并发读写安全控制这里需要咱们自己控制synchronized (TicketService.class) {// 双重判定的方式避免重复创建if ((localLock localLockMap.getIfPresent(lockKey)) null) {// 创建本地公平锁并放入本地公平锁容器中localLock new ReentrantLock(true);localLockMap.put(lockKey, localLock);}}}localLock.lock();try {RLock lock redissonClient.getFairLock(lockKey);lock.lock();try {return executePurchaseTickets(requestParam);} finally {lock.unlock();}} finally {localLock.unlock();}
}文末总结
希望通过以上两个优化方向的讲解能给大家对分布式锁的设计带来新的思路最后再给大家引用一位大佬的话 技术设计中不存在“银弹”。选择技术选型往往会有得失多方面权衡后选择出一个适合项目的使用即可。 一起加油吧陌生的程序人