1.分布式相关理论(重点)
1.1 CAP定理
编辑
CAP定理,也称为布鲁尔定理,是分布式计算领域的一个基本原理,用于描述在分布式系统设计中的三个基本要素:一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)之间的权衡关系。
CAP定理表明,在一个分布式计算系统中,不可能同时满足一致性、可用性和分区容错性这三个要求,最多只能同时满足其中的两个。具体而言:
- 一致性(Consistency)指的是在分布式系统中的所有节点,在同一时间点上是否具有相同的数据副本。如果系统在更新数据后,所有节点立即能够看到最新的数据,那么系统就是强一致性的;如果系统在更新数据后,不同节点的数据同步存在一定的延迟,那么系统就是弱一致性的。
- 可用性(Availability)指的是系统能够在有限的时间内对外提供服务,即系统能够处理请求并返回合理的响应。可用性要求系统在面对故障或异常情况时,仍能够保持正常的运行状态,对外提供服务。
- 分区容错性(Partition Tolerance)指的是系统能够在面对网络分区(节点之间的通信故障)时,仍能够继续运行,并保持数据一致性和可用性。分布式系统中的网络分区是不可避免的,因此分区容错性是必须考虑的因素。
根据CAP定理,分布式系统设计者需要在一致性、可用性和分区容错性之间进行权衡取舍。根据实际需求和应用场景的不同,可以选择满足不同程度的一致性和可用性。
需要注意的是,CAP定理是一个理论上的原则,并没有要求系统只能满足其中的两个要求,实际系统可以根据具体需求和技术手段做出更细致的权衡和设计。此外,CAP定理只描述了三个基本要素之间的关系,并没有涉及性能、性价比等其他方面的考虑。在实际系统设计中,还需要结合具体的业务需求和技术实现来进行综合权衡和设计。
编辑
- CP 架构的系统:
(1)Apache ZooKeeper:ZooKeeper是一个开源的分布式协调服务,提供了高可用、一致性和持久性的特性,用于构建分布式系统。
(2)Consul是由HashiCorp开发的服务发现和配置工具,提供了服务发现、健康检查、KV存储和多数据中心的功能。
- AP架构的系统:
(1)Redis:Redis是一个开源的内存数据库,它提供了高性能的键值存储和支持复制、分区容错等特性,强调了可用性和分区容错性。
(2)Elasticsearch:Elasticsearch是一个分布式的搜索和分析引擎,具有高可用性和分区容错性,适用于构建实时搜索和分析系统。
(3)Apache Kafka:Kafka是一个分布式流处理平台,具有高可用性和分区容错性,被广泛用于构建实时数据管道和事件流处理系统。
(4)Nacos: nacos 是一个开源的动态服务发现、配置管理和服务管理平台。它提供了服务注册与发现、动态配置管理、服务健康检查和动态DNS等功能。默认AP,可手动改为CP 。
2.2 BASE理论
BASE理论是分布式系统设计中的一个原则,用于描述在大规模分布式系统中追求可用性和性能的策略。BASE是"Basically Available, Soft state, Eventually consistent"的缩写。
- Basically Available (基本可用):系统在面对部分故障或异常情况时,仍能够保持基本的可用性,即系统能够处理请求并返回合理的响应。基本可用性是相对于完全不可用而言的,系统可以通过降级、限流等机制来保持基本的可用性。
- Soft state(软状态):系统中的数据状态可以在一段时间内是不完全一致的。在分布式系统中,由于存在网络延迟、节点故障等因素,各节点之间的数据同步可能存在一定的延迟。软状态的特点是数据状态可以在一段时间内是部分一致的,但最终会达到一致状态。
- Eventually consistent (最终一致性):系统中的数据最终会达到一致状态,但在某个时间点上可能存在部分不一致的情况。最终一致性是相对于强一致性而言的,系统可以通过异步复制、延迟补偿等机制来实现数据的最终一致性。
BASE理论的目标是通过放宽一致性要求,追求更高的可用性和性能。相对于传统的ACID(原子性、一致性、隔离性、持久性)事务模型,BASE理论更适用于大规模分布式系统,可以提供更好的可扩展性和容错性。
需要注意的是,BASE理论并不是一个具体的算法或实现,而是一种思想和原则,可以根据具体的业务需求和技术实现进行灵活的应用。在实际系统设计中,需要综合考虑一致性、可用性、性能等因素,选择适合的策略并进行合理的权衡。
2. 服务端设备运行日志
2.1 需求分析
当用户支付成功后,服务端需要向设备发送出货指令。设备收到出货指令后,执行出货,并将结果发送给服务端。
我们需要随时掌握有哪些数据是成功了,有哪些是失败了,有哪些是没有得到结果。
2.2 实现思路
我们需要在服务端建立一个设备发送日志,当发送给设备指令时新增记录,状态为0 ,当收到结果时修改状态,成功为1 ,失败为2 。
这个表就是 tb_vendout_running
编辑
2.3 代码实现
2.3.1 发货添加日志
(1)创建DTO
typescript
@Data
public class VendoutRunningDTO {
private String orderNo;//订单编号
private String innerCode;//售货机编号
private String channelCode;//货道编号
private String status;//状态
}
(2)feign接口类 VMService 新增方法定义
less
/**
* 新增售货机日志
* @param vendoutRunning
* @return 是否成功
*/
@PostMapping("/vendoutRunning")
public boolean addVendoutRunning(@RequestBody VendoutRunningDTO vendoutRunning);
(3)VmServiceFallbackFactory 编写熔断方法
typescript
@Override
public boolean addVendoutRunning(VendoutRunningDTO vendoutRunning) {
return false;
}
(4)创建运行日志状态
arduino
/**
* 运行日志状态
*/
public class VMRuningStatus {
public static final String VENDOUT_PREP = "0"; //准备发货
public static final String VENDOUT_COMP = "1";//完成发货
public static final String VENDOUT_FAIL = "2";//发货失败
}
(5)在发送出货指令时,向售货机运行日志插入记录,状态为0 (无结果)
修改CallBackServiceImpl的successPay方法,在发送消息前 , 添加添加售货机运行日志
scss
@Override
public void successPay(String orderSn) {
log.info("支付成功回调{}",orderSn);
OrderEntity orderEntity = orderService.getByOrderNo(orderSn);
if(orderEntity!=null){
if(orderEntity.getStatus().equals( OrderStatus.ORDER_STATUS_CREATE )){
orderEntity.setStatus(OrderStatus.ORDER_STATUS_PAYED); //订单状态 已支付
orderEntity.setPayStatus(PayStatus.PAY_STATUS_PAYED) ;//支付状态 成功
//查询出货货道
ChannelVO channelVO = vmService.getChannel(orderEntity.getInnerCode(), orderEntity.getSkuId());
orderEntity.setChannelCode( channelVO.getChannelCode() );
orderService.updateById( orderEntity );
//添加服务端运行日志
VendoutRunningDTO vendoutRunning =new VendoutRunningDTO();
BeanUtils.copyProperties( orderEntity,vendoutRunning );
vendoutRunning.setStatus(VMRuningStatus.VENDOUT_PREP); //状态:准备发货
vmService.addVendoutRunning(vendoutRunning );
//构建报文并发送
VendoutDTO vendoutDTO=new VendoutDTO(); //报文封装对象 (数据传输对象)
BeanUtils.copyProperties( orderEntity,vendoutDTO );
elegentAC.publish(TopicConfig.getVendoutTopic( orderEntity.getInnerCode() ) , vendoutDTO);
}
}
}
2.3.2 处理出货结果
在售货机微服务接到正常的出货结果时,更改日志状态为1 (正常)
在售货机微服务接到异常的出货结果时,更改日志状态为2 (异常)
修改售货机微服务 dkd_vms_service的VendOutResultHandler方法
less
/**
* 售货机微服务处理出货结果
*/
@Topic(TopicConfig.VMS_RESULT_TOPIC)
@Slf4j
public class VendoutResultHandler implements ACHandler<VendoutResultDTO> {
@Autowired
private ChannelService channelService;
@Autowired
private VendoutRunningService vendoutRunningService;
@Override
public void process(String s, VendoutResultDTO vendoutResultDTO) throws Exception {
//出货成功扣减库存
if(vendoutResultDTO.isSuccess()){
log.info( "出货成功,扣减库存:{}", vendoutResultDTO );
ChannelEntity channelEntity = channelService.getChannelInfo(vendoutResultDTO.getInnerCode(), vendoutResultDTO.getChannelCode());
channelEntity.setCurrentCapacity( channelEntity.getCurrentCapacity()-1 );
channelService.updateById( channelEntity);
vendoutRunningService.updateStatus(vendoutResultDTO.getOrderNo(), VMRuningStatus.VENDOUT_COMP);//发货成功
}else{
vendoutRunningService.updateStatus(vendoutResultDTO.getOrderNo(), VMRuningStatus.VENDOUT_FAIL);//发货失败
}
}
}
2.3.3 最终一致性补偿数据
因为服务端和设备需要通过网络进行通信,这期间可能会因为网络原因无法将出货指令送达到设备,也有可能会在设备出货后因为网络故障无法将结果上报给服务端。这两种情况,都可能导致运行日志的状态为0 。
那么,遇到这种情况如何处理呢?我们采用的方案是最终一致性。
服务端和设备端都保存有日志记录,设备端在接收到指令后,也会记录在日志中。设备在上报前和上报后都会做记录,如果设备端的日志记录上报状态为未上报,则是因为网络原因导致无法及时上报消息。
对于这些没有成功上报的消息,设备端会每间隔一段时间再次进行状态的上报。这样一旦网络通畅了,就会实现数据的最终一致性。
3.分布式锁技术研究(重点)
3.1 什么是分布式锁
分布式锁是一种用于协调分布式系统中并发访问的机制。在分布式系统中,多个节点可能同时访问共享资源,为了避免数据不一致或竞争条件的发生,需要一种机制来确保在某个节点操作共享资源时,其他节点不能同时访问该资源。
分布式锁通过在分布式系统中引入一个全局的锁来实现这一目的。当一个节点需要访问共享资源时,它首先尝试获取分布式锁,如果成功获取到锁,则可以执行相应的操作;如果获取锁失败,则需要等待锁释放后再次尝试。
分布式锁可以使用各种技术和算法来实现,常见的实现方式包括基于数据库、基于缓存、基于ZooKeeper等。这些实现方式通常要考虑高可用性、性能和可靠性等因素。
使用分布式锁可以有效地控制分布式系统中的并发访问,确保数据的一致性和正确性。然而,分布式锁的设计和使用需要仔细考虑各种情况,避免死锁、性能瓶颈和单点故障等问题的发生。
编辑
3.2 分布式锁的分类
- 按实现方式分类:
-
- 基于数据库的分布式锁:使用数据库的事务和唯一约束来实现分布式锁,通常使用表中的一行记录表示一个锁。
- 基于缓存的分布式锁:利用分布式缓存(如Redis)的原子性操作和过期时间特性来实现分布式锁。
- 基于Zookeeper的分布式锁:利用ZooKeeper这样的分布式协调服务来实现分布式锁,通过创建和删除节点来实现锁的获取和释放。
- 基于Consul 的分布式锁
- 按特性分类:
-
- 阻塞锁:获取锁失败时,请求线程会被阻塞直到获取到锁为止。
- 非阻塞锁:获取锁失败时,请求线程会立即返回而不会被阻塞,可以通过轮询或者回调方式来获取锁。
- 按是否可重入分类:
-
- 可重入锁 :允许同一个线程多次获取同一把锁,通常用于递归调用或者嵌套调用的场景。
可重入锁是一种特殊的锁,允许同一个线程在持有锁的情况下多次获取该锁,而不会被自己所持有的锁所阻塞。这种特性使得线程可以在递归调用或者嵌套调用的情况下安全地使用锁,而不必担心死锁或者竞争条件的问题。
在实现可重入锁时,通常需要记录当前锁的持有者以及持有次数。当线程再次获取同一把锁时,系统会检查当前线程是否已经持有该锁,如果是,则增加持有次数;如果不是,则阻塞或者返回失败。 - 不可重入锁
- 可重入锁 :允许同一个线程多次获取同一把锁,通常用于递归调用或者嵌套调用的场景。
- 按是否公平分类:
-
-
公平锁 :
公平锁是一种锁,它保证锁的获取按照请求的顺序进行分配,避免某些线程长期等待而无法获取锁的情况,从而避免了"饥饿"现象的发生。
在公平锁中,当有多个线程竞争同一把锁时,锁会按照请求的顺序分配给等待时间最长的线程,而不是随机分配给任意一个等待的线程。这样可以确保所有线程都有公平的机会获取锁,避免了某些线程长期无法获取锁的情况。
编辑
-
非公平锁 当有多个线程竞争同一把锁时 ,随机分配给任意一个等待的线程。
-
编辑
-
3.3 实现分布式锁的几种方式
3.3.1 基于数据库实现的分布式锁(了解)
创建锁表 :在数据库中创建一张用于存储锁信息的表,例如名为distributed_lock,包括以下字段:
lock_name:锁的名称,用于区分不同的锁。holder:锁的持有者标识,可以是进程ID、线程ID或者唯一的标识符。expire_time:锁的过期时间,避免锁被长时间占用。
sql
CREATE TABLE distributed_lock (
lock_name VARCHAR(255) PRIMARY KEY,
holder VARCHAR(255),
expire_time TIMESTAMP
);
获取锁:在需要获取锁的地方,通过数据库事务来尝试插入一条锁记录,如果插入成功则表示获取到了锁,否则表示锁已经被其他进程持有。
sql
INSERT INTO distributed_lock (lock_name, holder, expire_time) VALUES ('my_lock', 'process_id', NOW() + INTERVAL 10 SECOND);
释放锁:在任务执行完成或者锁过期时,通过事务操作来删除对应的锁记录,释放锁资源。
ini
DELETE FROM distributed_lock WHERE lock_name = 'my_lock' AND holder = 'process_id';
处理锁超时:定期清理过期的锁记录,可以通过定时任务或者后台进程来实现。
sql
DELETE FROM distributed_lock WHERE expire_time < NOW();
优缺点:
优点:不需要引入其它中间件。
缺点:对数据库压力较大,执行效率较低。
3.3.2 Redis分布式锁-setNx命令(了解)
redis实现分布式锁可以使用Redis的setNx命令来实现,它的原理是这样的:
编辑
如果你使用的是SpringDataRedis ,实现的方式也比较简单:
如果是加锁,代码如下:
ini
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, 1, 60, TimeUnit.SECONDS);
这里面实际就是调用了redis的setnx ,setnx 大致原理,主要依托了它的key不存在才能set成功的特性。
如果是释放锁,比较简单的是直接把这个key删除掉。但是这样一来删除锁和加锁的不一定是同一个进程,所以我们需要释放锁的时候判断当前进程和加锁的进程是不是同一个进程,这就需要在删除前再查询一次,而这样一来就不是一个原子操作了。如果释放锁想要成为一个原子操作,比较常见的方案就是通过调用lua脚本来实现。
释放锁的lua脚本是这样写的:
lua
if redis.call('get', KEYS[1]) == ARGV[1]
then
-- 执行删除操作
return redis.call('del', KEYS[1])
else
-- 不成功,返回0
return 0
end
在这个Lua脚本中,假设锁的键名为KEYS[1],并且锁的值为ARGV[1]。脚本首先通过get命令获取锁的当前值,然后判断锁的当前值是否与传入的值相同。如果相同,则使用del命令删除该键,表示成功释放锁;如果不相同,则返回0,表示释放失败。
执行释放锁的lua脚本
javascript
// 解锁脚本
DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();
unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockDel.lua")));
// 执行lua脚本解锁
redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);
【知识点小节】
- 如何实现redis分布式锁?
(1)我们可以使用Redis的setNx命令加锁,对于我们常用的SpringDataRedis框架来说,可以使用setIfAbsent方法来实现加锁。
(2)这种方式的释放锁不具备原子性,所以我们需要将释放锁的操作放到lua脚本,在代码中调用lua脚本来实现。我们可以使用redisTemplate的execute方法来调用lua脚本。
redis实现分布式锁可以使用Redis的setNx命令来实现,它的原理是这样的:
编辑
如果你使用的是SpringDataRedis ,实现的方式也比较简单:
如果是加锁,代码如下:
ini
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, 1, 60, TimeUnit.SECONDS);
这里面实际就是调用了redis的setnx ,setnx 大致原理,主要依托了它的key不存在才能set成功的特性。
如果是释放锁,比较简单的是直接把这个key删除掉。但是这样一来删除锁和加锁的不一定是同一个进程,所以我们需要释放锁的时候判断当前进程和加锁的进程是不是同一个进程,这就需要在删除前再查询一次,而这样一来就不是一个原子操作了。如果释放锁想要成为一个原子操作,比较常见的方案就是通过调用lua脚本来实现。
释放锁的lua脚本是这样写的:
lua
if redis.call('get', KEYS[1]) == ARGV[1]
then
-- 执行删除操作
return redis.call('del', KEYS[1])
else
-- 不成功,返回0
return 0
end
在这个Lua脚本中,假设锁的键名为KEYS[1],并且锁的值为ARGV[1]。脚本首先通过get命令获取锁的当前值,然后判断锁的当前值是否与传入的值相同。如果相同,则使用del命令删除该键,表示成功释放锁;如果不相同,则返回0,表示释放失败。
执行释放锁的lua脚本
javascript
// 解锁脚本
DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();
unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockDel.lua")));
// 执行lua脚本解锁
redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);
【知识点小节】
- 如何实现redis分布式锁?
(1)我们可以使用Redis的setNx命令加锁,对于我们常用的SpringDataRedis框架来说,可以使用setIfAbsent方法来实现加锁。
(2)这种方式的释放锁不具备原子性,所以我们需要将释放锁的操作放到lua脚本,在代码中调用lua脚本来实现。我们可以使用redisTemplate的execute方法来调用lua脚本。
3.3.3 Redisson组件封装实现
因为setNx加锁存在几个弊端:
(1)释放锁调用lua脚本实现稍繁琐
(2)不可重入
(3)没有实现续期
我们使用Redisson组件可以很轻松地解决以上问题。 Redisson组件提供了可重入的,可以实现续期的分布式锁。Redisson组件为你提供了一个"看门狗"机制。
在项目中使用,参考以下步骤:
(1) 引入依赖
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
(2)编写配置类
kotlin
@Configuration
public class RedissionConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.password}")
private String password;
private int port = 6379;
@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useSingleServer().
setAddress("redis://" + redisHost + ":" + port).
setPassword(password);
return Redisson.create(config);
}
}
(3)加锁和释放锁
引入RedissonClient
java
@Resource
private RedissonClient redissonClient;
通过RLock加锁
ini
RLock rLock = redissonClient.getLock(lockName);
boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS);
if (isLocked) {
// TODO: 如果加锁成功
}
通过RLock释放锁
ini
RLock rLock = redissonClient.getLock(lockName);
boolean isLocked = rLock.unLock();
【知识点小节】
如何使用Redisson组件实现分布式锁?
(1)使用Redisson.create方法创建RedissonClient。
(2)通过redissonClient.getLock方法获取锁对象RLock。
(3)通过调用RLock的tryLock方法加锁
(4)通过调用RLock的unLock方法释放锁
3.3.4 RedLock算法的实现
为什么需要使用红锁(RedLock)?
(1)如果我们连接的是一个单节点的Redis,有可能因为Redis宕机导致业务系统瘫痪。
(2)如果我们连接的是一个集群环境,有可能因为脑裂问题导致分布式锁失效。
脑裂 指的是因为网络通讯中断导致一个集群分裂为两个集群的现象。
编辑
(3)如果我们使用红锁算法,它所连接的多个节点,并不是一个集群,而是独立的,它的实现原理就是对每个节点依次加锁,超过半数成功,不超过半数失败。
编辑
如何使用红锁,我们看代码:
(1) 引入redisson依赖 ,和上边的一样
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
(2)编写配置类,这个有些区别
scss
@Configuration
public class RedissionConfig {
/**
* 红锁地址列表
*/
@Value("${elegent.lock.address}")
private String[] address;
@Bean
public RedissonClient[] getRedisson() {
//初始化
RedissonClient[] redissonClients=new RedissonClient[ address.length ];
for(int i=0;i<redisLockConfig.getAddress().length;i++){
Config config = new Config();
config.useSingleServer().setAddress( "redis://"+redisLockConfig.getAddress()[i] );
redissonClients[i] = Redisson.create(config);
}
return redissonClients;
}
}
(3)加锁和释放锁
先要初始化,假设我们有五个节点
ini
//客户端
private RedissonClient [] redissonClients;
@PostConstruct
public void init() {
//初始化
redissonClients=new RedissonClient[ address.length ];
for(int i=0;i<redisLockConfig.getAddress().length;i++){
Config config = new Config();
config.useSingleServer().setAddress( "redis://"+redisLockConfig.getAddress()[i] );
redissonClients[i] = Redisson.create(config);
}
}
private RedissonRedLock getRedissonRedLock(String lockName){
RLock lock0 = redissonClients[0].getLock(lockName);
RLock lock1 = redissonClients[1].getLock(lockName);
RLock lock2 = redissonClients[2].getLock(lockName);
RLock lock3 = redissonClients[3].getLock(lockName);
RLock lock4 = redissonClients[4].getLock(lockName);
return new RedissonRedLock(lock0,lock1,lock2,lock3,lock4);
}
业务代码中加锁和释放锁
scss
//红锁
RedissonRedLock redLock=getRedissonRedLock(lockName);
redLock.tryLock(60 ,TimeUnit.SECONDS);//加锁 参数1是过期时间 ,如果给-1则表示续期
redLock.unlock();//释放锁
【知识点小节】
如何使用红锁?
(1)创建每个redis节点的RedissonClient
(2)通过每个redis节点的RedissonClient构建RedissonRedLock 。
(3)通过RedissonRedLock的实例的tryLock加锁,unlock释放锁。
3.4 Elegent-lock
3.4.1 Elegent-lock简介
这是一个基于springboot的优雅的分布式锁组件。使用这个组件可以让你更轻松、更优雅地在项目中集成分布式锁,让你更专注业务代码的开发。它目前支持redis分布式锁、consul分布式锁两种实现方式,可以通过更改配置自由切换而不需要更改业务代码。
开源项目地址: gitee.com/chuanzhiliu...
3.4.2 Elegent-lock快速入门
集成
1.在项目中引入依赖
xml
<dependency>
<groupId>cn.elegent.lock</groupId>
<artifactId>elegent-lock-redis</artifactId>
<version>1.1.0</version>
</dependency>
2.在项目配置文件添加配置,示例如下:
yaml
elegent:
lock:
type: redis
host: 127.0.0.1:6379
wait: 10
配置说明:
(1)type: 分布式锁类型,可选值:consul、redis,默认值是redis。
(2)host: 分布式锁中间件的部署地址,默认值为127.0.0.1。
(3)wait: 等待超时时间,单位秒,默认值10。表示在获取不到锁的时候会在此时间内会进行重试。
代码方式
类中引入ElegentLock
java
@Autowired
private ElegentLock elegentLock;
方法调用以下方法实现加锁与释放锁
csharp
//参数说明: 锁名称,过期时间,是否自旋
boolean b = elegentLock.lock(name,60,false);
//todo: 业务逻辑
System.out.println("业务逻辑"+b);
elegentLock.unLock(name);
注解方式
在方法上添加注解@ELegentLock(lockName = "test",isSpin= true,ttl=10) 即可。
lockName为锁名字,实际加锁会以lockName+方法参数作为锁key。
always =true,加锁,如果锁被占用会通过自旋方式不断尝试,直到加成功为止。
always =false,(默认值)加锁,只尝试一次。推荐使用此选项。
我们可以运行Elegent提供的demo代码,进行快速学习。
3.5 基于Consul分布式锁
3.5.1 Consul简介
Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 Consul是分布式的、高可用的、可横向扩展的。它具备以下特性 :
服务发现:consul通过DNS或者HTTP接口使服务注册和服务发现变的很容易。
健康检查:健康检测使consul可以快速的告警在集群中的操作。
键/值存储:一个用来存储动态配置的系统。提供简单的HTTP接口,可以在任何地方操作。
多数据中心:无需复杂的配置,即可支持任意数量的区域。
一句话概况:
Consul既可以用于注册中心和配置中心,也可以做keyValue存储。使用Consul做分布式锁的底层原理就是keyValue存储。
课程提供了配套的 consul 本地运行环境
3.5.2 项目为什么使用Consul做分布式锁?
Consul的分布式锁与Redis分布式锁有什么不同?项目为什么使用Consul做分布式锁?
一句话概况:
因为Consul分布式锁是CP架构的,使用 Raft 算法来保证一致性。相比之下,Redis由于是AP架构,可能因为脑裂造成数据不一致,如果采用Redis红锁性能又很差。所以,当时我们权衡利弊,决定采用Consul分布式锁。
红锁会有弊端。时间复杂度变高(响应时间边长,吞吐量变低) ,空间复杂度变高了。
3.5.3 Consul做分布式锁快速入门
修改配置文件 为consul
4 分布式锁解决超卖问题
4.1 问题分析
首先我们先说为什么会产生超买的问题。
如果售货机是带屏幕的,理论上来说是不会产生超卖问题的。因为带屏幕的售货机实际中就只能是独占类型的操作,但是如果是不带屏幕的售货机,用户就需要扫描售货机上的二维码来进行购买操作, 那么假设有两个用户在相近的时间点在同一台售货机购买了同一个商品, 而恰好这件商品只有一件了,就会可能产生超卖的问题。
编辑
为什么会超卖呢?我们看一下下面的图:
这是一个用户的购买时序图
编辑
当用户下单时,会判断当前要购买的商品在该售货机的库存,如果当前库存为1件,也是可以下单的,用户这个时候下单并支付,支付成功后,支付系统回调我们的订单微服务,订单微服务向售货机发送出货指令,售货机终端执行出货,并向服务端上报结果,服务端的售货机微服务再扣减库存。这期间其实至少也需几秒左右的时间才能完成。而如果在这期间,另外一个人也着急购买商品,扫描并下单购买同一个商品,那么这个时候第一个人购买的流程还没有结束,库存还没有扣减,所以显示的商品仍然还有一件,那他仍然可以下单。等他支付完成后,售货机没有商品可以出货了,这个时候就产生了超卖。
编辑
这样我们就会产生超卖的现象。
4.2 实现思路
那么如何解决超卖现象呢?我们就需要使用分布式锁来解决。
user1在下单前需要加锁,在扣减库存后要释放锁。这样,就确保user2在下单时上一个流程是已经跑完的,此时的库存就是准确的,就不会产生超卖的问题了。
编辑
4.3 代码实现
4.3.1 订单微服务加锁
(1)在订单微服务的pom文件引入依赖
xml
<dependency>
<groupId>cn.elegent.lock</groupId>
<artifactId>elegent-lock-consul</artifactId>
<version>1.1.0</version>
</dependency>
(2)在订单微服务的配置(配置中心)中添加以下配置
yaml
elegent:
lock:
type: consul
host: 127.0.0.1:8500
(3)在OrderServiceImpl的createOrder方法中添加分布式锁代码:
typescript
@Autowired
private ElegentLock eLegentLock;
@Override
public OrderEntity createOrder(PayVO payVO,String platform) {
//判断库存
if( !vmService.hasCapacity(payVO.getInnerCode(), Long.valueOf(payVO.getSkuId())) ){
throw new LogicException("商品库存不足");
}
//加锁,判断上次交易是否完成
boolean lock = eLegentLock.lock(payVO.getInnerCode() + "-" + payVO.getSkuId(), 60, false);
if(!lock){
throw new LogicException("上一笔交易未完成,请稍后!");
}
..................................
}
4.3.2 售货机微服务释放锁
(1)在售货机微服务的pom文件引入依赖
xml
<dependency>
<groupId>cn.elegent.lock</groupId>
<artifactId>elegent-lock-consul</artifactId>
<version>1.1.0</version>
</dependency>
(2)在售货机微服务的配置(配置中心)中添加以下配置
yaml
elegent:
lock:
type: consul
host: 127.0.0.1:8500
(3)在VendOutResultHandler的process方法中添加释放分布式锁代码:
java
@Autowired
private ElegentLock elegentLock;
@Override
public void process(String s,VendoutResultDTO vendoutResultDTO) throws Exception {
log.info("接收到出货结果,{}", vendoutResultDTO);
..........
//释放锁
elegentLock.unLock( vendoutResultDTO.getInnerCode()+"-"+vendoutResultDTO.getSkuId() );
}
5.超时订单处理方案-延迟消息
5.1 需求分析
有很多时候,用户下单后,并不一定会支付。如果用户一直不支付,那么这个订单岂不是一直处于未支付状态,这并不利于我们的订单管理,我们通常的做法是设定一个时效,超过这个时间,需要将这个订单更改为无效状态,并且在支付平台将这笔交易关闭掉。
5.2 实现思路
(1)在下单时,通过AC框架发送异步消息,延迟时间为5分钟。发送内容为订单号
协议封装: OrderCheck 封装的是订单号
主题封装: TopicConfig.ORDER_CHECK_TOPIC
如果延迟5分钟,前缀为 $delayed/300/
(2)在订单服务中接收延迟消息, 从协议中解析出订单号, 查询订单,如果此订单未支付则修改为无效订单,并关闭此支付交易单。
5.3 代码实现
5.3.1 主题定义与协议封装
(1)TopicConfig定义延迟订单主题
arduino
/**
* 延迟订单主题
*/
public final static String ORDER_CHECK_TOPIC = "server/order/check";
(2)service_common定义OrderCheckDTO,用于封装订单号
arduino
/**
* 订单延迟检查协议类
*/
@Data
public class OrderCheckDTO {
private String orderNo;
}
5.3.2 发送延迟消息
OrderServiceImpl引入
java
@Autowired
private ElegentAC elegentAC;
在OrderServiceImpl 的createOrder方法结尾处添加以下代码
scss
//将订单放到延迟队列中,5分钟后检查支付状态!!!!!!!!!!!!!!!!!!
OrderCheckDTO orderCheck = new OrderCheckDTO();
orderCheck.setOrderNo(orderEntity.getOrderNo());
try {
elegentAC.delayPublish(TopicConfig.ORDER_CHECK_TOPIC,orderCheck,300);
} catch (Exception e) {
log.error("send to emq error",e);
}
5.3.3 接收延迟消息
在订单服务项目中实现接收到该消息的处理代码:
less
/**
* 订单超时处理
*/
@Topic(TopicConfig.ORDER_CHECK_TOPIC)
@Slf4j
public class OrderCheckHandler implements ACHandler<OrderCheckDTO> {
@Autowired
private OrderService orderService;
@Autowired
private ElegentPay elegentPay;
@Override
public void process(String s, OrderCheckDTO orderCheck) throws Exception {
if(orderCheck == null || Strings.isNullOrEmpty(orderCheck.getOrderNo())) return;
//查询订单
OrderEntity orderEntity = orderService.getByOrderNo(orderCheck.getOrderNo());
if(orderEntity == null) return;
if(orderEntity.getStatus().equals(OrderStatus.ORDER_STATUS_CREATE)){ //如果是未支付
log.info("订单无效处理 订单号:{}",orderCheck.getOrderNo());
orderEntity.setStatus(OrderStatus.ORDER_STATUS_INVALID); //无效状态
orderService.updateById(orderEntity);
//关闭支付
elegentPay.closePay( orderEntity.getOrderNo(),orderEntity.getPayType() );
}
}
}
总结:
(1)请问你如何理解CAP定理 ?
CAP 定理阐述一个观点: C(一致性)A(可用性)P(分区容错) 不能同时满足。
CP系统: ZK \ consul
AP系统: redis NACOS
(2)什么是BASE理论
基本可用 软状态 最终一致性。
(3)你在项目中是否使用过分布锁?
是,用过。我们使用CONSUL的原因。[分析分布式锁单机版单点故障、红锁性能损失角度逐步分析 ]
(4)你在项目中是否使用过线程池技术?
是,用过。
(5)分布式锁有哪些?
(6)REDISSION分布式锁你是怎么实现的? 答:常见方法.引出框架和CONSUL分布式锁。
(7)延迟消息你项目中场景是什么?