当前位置:网站首页>微服务系统设计——分布式锁服务设计
微服务系统设计——分布式锁服务设计
2022-06-27 04:11:00 【庄小焱】
摘要
对于分布式项目来说分布式锁是一个中高级工程师的必备技能。在停车系统中的会员办理月卡或签到累积的积分,可以在指定时间段内兑换商场优惠券,由于数量有限,时间有限,兑换操作相当集中,如果按正常流程处理的话,肯定会出现超兑的情况。比如只有 5000 张券,结果兑换出 8000 张,这对商场来说是一笔经济损失。
为防止超兑,自然做法是按总量一个接一个兑换,至到兑换完,但多并发的情况下如何保证还一个一个兑换呢?自然而然就会想到锁上面来。提及锁,你脑海是不是出现了一堆关于锁的场景:死锁、互斥锁、乐观锁、悲观锁等等,本节介绍分布式锁,它主要应用于分布式系统下面,单体应用基本不会涉及。
一、常见的分布式锁的实现机制
常见的实现方法
- 基于数据库的分布式锁
- 基于Redis的分布式锁
- 基于Zookeeper的分布式锁
采用数据库实现分布式锁,还记得前面分布式定时服务设计吗?里面就用到分布式锁。为保证指定时刻下多实例定时任务的执行,优先通过 ShedLock 的方式获取锁,锁产生在公共存储库中,生成一条新记录来告诉其它集群中其它实例,我正在执行,其它实例获取到这个状态后,自动跳过不再执行,来保证同一时刻只有一个任务在执行。
采用 Redis 实现分布式锁。Redis 提供了 setnx 指令,保证同一时刻内只有一个请求针对同一 key 进行 setnx 操作,鉴于 Redis 是单线程模式,依旧是先到先得,晚到不得,通过这个操作可以实现排它性的操作。但此做法存在漏洞,操作 key 后,指令发起方挂掉的话,这个 key 就永远不能被操作了。稍做改进,给 key 设置失效时间,这样就可以到期自动释放,供其它操作。但依旧有漏洞,在 setnx 后,发起 expire 前服务挂了,这种方式依旧与第一处方式类似。细查 Redis 官方指令后,发现 set 指令后还跟有 [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
等选项,可以针对第二种方式用此方式进一步改进。在单实例 Redis 的情况下,在实例可用的情况下,取锁、释放锁操作已经基本可用。
Redis 另外提供了一种 Redlock 算法来实现分面式锁,有兴趣的朋友可看原文(中文版本),在单实例无法保证可用的情况下,通过集群中多实例来有效防止单点故障导致锁不可用。大致意思是同某一时刻,向所有实例发起加锁请求,如果获取到 N/2+1 个锁表示成功,否则失败并自动解锁所有实例,到达锁失效期后同样去解锁所有实例。如果是自己去实现这一套算法的话,想必还是比较复杂的,庆幸的是有非常好的成品,已经帮我们完成了。这就是本篇要提到的 Redission 客户端,里面有 Redlock 分布式锁的完整实现。
二、Redisson
Redis 的三大 Java 客户端之一,其它两个是:Jedis 和 Lettuce(SpringBoot 2.x 之后就将默认集成的 Jedis 客户端替换成 Lettuce)。不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务。Redisson 的宗旨是促进使用者对 Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
2.1 引入 Redisson
由于我们使用的框架是 Spring Boot 搭建的,这里同样采用 starter 的方式引入(不再需要 spring-data-redis 模块):
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.6</version>
</dependency>
配置文件采用 redis 的默认配置方式,可以兼容:
#redis config
spring.redis.database=2
spring.redis.host=localhost
spring.redis.port=16479
#default redis password is empty
spring.redis.password=zxcvbnm,./
spring.redis.timeout=60000
spring.redis.pool.max-active=1000
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=10
spring.redis.pool.min-idle=5
2.2 代码编写、测试
这里编写了一个启动类,将本次兑换优惠券总可兑换数量写入缓存,每次采用原子操作进行减少。
@Component
@Order(0)
public class StartupApplicatonRunner implements ApplicationRunner {
@Autowired
Redisson redisson;
@Override
public void run(ApplicationArguments args) throws Exception {
RAtomicLong atomicLong = redisson.getAtomicLong(ParkingConstant.cache.grouponCodeAmtKey);
atomicLong.set(ParkingConstant.cache.grouponCodeAmt);
}
}
在兑换逻辑中,判断优惠券可用数量,兑换结束后数量减 1:
@Autowired
Redisson redisson;
@Override
public int createExchange(String json) throws BusinessException {
Exchange exchange = JSONObject.parseObject(json, Exchange.class);
int rtn = 0;
// 兑换类型有两部分,0 是商场优惠券,1 是洗车券,这是作了简单区分
if (exchange.getCtype() == 0) {
RAtomicLong atomicLong = redisson.getAtomicLong(ParkingConstant.cache.grouponCodeAmtKey);
// 获取锁
RLock rLock = redisson.getLock(ParkingConstant.lock.exchangeCouponLock);
// 锁定,默认 10s 不主动解锁的话,自动解锁,防止出现死锁的情况。正常情况下可基于 redisson 获取 redLock 处理,更加安全,本测试基于单机 redis 测试。
rLock.lock(1000, TimeUnit.SECONDS);
log.info("lock it when release ...");
// 判定可兑换数量,如果有就兑换,兑换结束数量减一
if (atomicLong.get() > 0) {
rtn = exchangeMapper.insertSelective(exchange);
atomicLong.decrementAndGet();
}
// 释放锁
rLock.unlock();
log.info("exchage coupon ended ...");
} else {
rtn = exchangeMapper.insertSelective(exchange);
}
log.debug("create exchage ok = " + exchange.getId());
return rtn;
}
简单测试,将 lock 时间设置个较长时间,利用断点来测试(也可以采用前面介绍到的 Postman 的方式进行并发测试)。
- 准备两个实例,一个实例构建成 jar 运行,一个实例在 IDE 中运行。
- 在 if 判定处打断点,在 IDE 中启动第一个实例,请求 lock 后,不向下运行。
- 启动 jar 实例,发起第二个请求,可以看到日志并未输出
_lock it when release …_
,而是一直在等待。 - 将 IDE 中的断点跳过,执行结束,自动释放锁。回头看 jar 实例的日志输出,可以看到两个日志正常输出。
这样就达到分布锁的目标,实际应用中锁定时间肯定比较短,否则服务会被拖垮,很类似秒杀的场景,但杀场景更复杂,还需要其它辅助手段,不能如此简单处理。文中只提到 Redission 的这一种锁的用法。
博文参考
边栏推荐
- [array]bm94 rainwater connection problem - difficult
- 2019LXMERT:Learning Cross-Modality Encoder Representations from Transformers
- 日志收集系统
- Ledrui ldr6035 usb-c interface device supports rechargeable OTG data transmission scheme.
- 第2章 关键技术介绍
- Fplan layout
- 010 C语言基础:C函数
- 办公室VR黄片,骚操作!微软HoloLens之父辞职!
- 笔记本电脑没有WiFi选项 解决办法
- Matlab | drawing of three ordinate diagram based on block diagram layout
猜你喜欢
ERP需求和销售管理 金蝶
WPF open source control library extended WPF toolkit Introduction (Classic)
渗透测试-文件上传/下载/包含
乐得瑞LDR6035 USB-C接口设备支持可充电可OTG传输数据方案。
使用promise的基本功能【四、Promise源码】
如何系统学习LabVIEW?
math_数集(数集符号)和集合论
2022-06-26:以下golang代码输出什么?A:true;B:false;C:编译错误。 package main import “fmt“ func main() { type
promise源码-class版本【三、Promise源码】【代码详细注释/测试案例完整】
百度飞桨“万有引力”2022首站落地苏州,全面启动中小企业赋能计划
随机推荐
如何让 EF Core 6 支持 DateOnly 类型
Learn crypto from Buu (Zhou Geng)
020 C语言基础:C语言强制类型转换与错误处理
007 C语言基础:C运算符
[BJDCTF2020]The mystery of ip
如何系统学习LabVIEW?
[promise I] introduction of promise and key issues of hand rolling
[BJDCTF2020]The mystery of ip
Fplan powerplan instance
A^2=E | 方程的解 | 这个方程究竟能告诉我们什么
2021:Graphhopper: Multi-Hop Scene Graph Reasoning for Visual Question Answering
Knowledge of iPhone certificate structure
Penetration test - directory traversal vulnerability
Six possible challenges when practicing Devops
Ledrui ldr6035 usb-c interface device supports rechargeable OTG data transmission scheme.
Interview-01
Système de collecte des journaux
深潜Kotlin协程(十五):测试 Kotlin 协程
2022-06-26: what does the following golang code output? A:true; B:false; C: Compilation error. package main import “fmt“ func main() { type
Cache comprehensive project - seckill architecture