分布式面试问题


Seata分布式事务

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。下面从基本概念、核心组件、工作模式、使用场景和简单示例几个方面详细介绍。

基本概念

在分布式系统中,一个业务操作可能会跨多个服务和数据库,传统的本地事务无法保证跨服务操作的数据一致性。Seata 就是为解决这类问题而生,它通过定义全局事务,将多个本地事务纳入全局事务的管理之下,确保在全局层面上数据的一致性。

核心组件

Seata 主要由三个核心组件构成:

  • TC(Transaction Coordinator):事务协调器,是 Seata 的核心组件,负责全局事务的注册、提交、回滚等事务状态的管理。
  • TM(Transaction Manager):事务管理器,负责定义全局事务的范围,开启、提交或回滚全局事务。
  • RM(Resource Manager):资源管理器,负责管理本地事务,向 TC 注册分支事务,在 TC 的指令下提交或回滚分支事务。

工作模式

Seata 提供了四种分布式事务解决方案:

  • AT 模式:是 Seata 默认的模式,基于支持本地 ACID 事务的关系型数据库。通过代理数据源自动生成回滚日志,在事务提交或回滚时根据日志进行相应操作,对业务代码侵入性低。
  • TCC 模式:即 Try - Confirm - Cancel,需要开发者手动实现三个阶段的业务逻辑。Try 阶段进行资源的预留,Confirm 阶段执行真正的业务操作,Cancel 阶段进行资源的释放。适用于对性能要求较高、业务逻辑复杂的场景。
  • SAGA 模式:将一个大的全局事务拆分成多个本地事务,每个本地事务都有对应的补偿操作。如果某个本地事务执行失败,则依次执行之前事务的补偿操作。适用于长流程、事务持续时间较长的场景。
  • XA 模式:基于数据库的 XA 协议,利用数据库本身的分布式事务能力。事务的提交和回滚由数据库管理系统负责,性能相对较低,但数据一致性强。

使用场景

  • 微服务架构下的跨服务业务操作:如电商系统中的下单流程,可能涉及库存服务、订单服务、支付服务等多个服务,使用 Seata 可以保证这些服务间数据的一致性。
  • 分布式系统中的数据同步:在不同数据库或服务之间进行数据同步时,使用 Seata 确保数据的完整性和一致性。

简单示例(AT 模式)

以下是一个基于 Spring Boot 和 Seata 的简单示例,模拟一个跨服务的扣减库存和创建订单的业务场景。

1. 引入依赖

pom.xml 中添加 Seata 相关依赖:

1
2
3
4
5
6
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>

2. 配置 Seata

application.yml 中配置 Seata:

1
2
3
4
5
6
7
8
9
10
11
12
13
seata:
tx-service-group: my_test_tx_group
registry:
type: nacos
nacos:
server-addr: localhost:8848
namespace: public
config:
type: nacos
nacos:
server-addr: localhost:8848
namespace: public

3. 业务代码

在服务调用的入口方法上添加 @GlobalTransactional 注解开启全局事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

@Autowired
private InventoryService inventoryService;

@Autowired
private OrderDao orderDao;

@GlobalTransactional
public void createOrder(String productId, int count) {
// 扣减库存
inventoryService.deduct(productId, count);
// 创建订单
orderDao.createOrder(productId, count);
}
}

4. 启动 Seata Server

从 Seata 官方仓库下载对应版本的 Seata Server,启动后,TC 服务就会正常运行。

通过以上步骤,就可以使用 Seata 的 AT 模式实现分布式事务的管理。

分布式锁

Redis 分布式锁是在分布式系统中保证多个进程或服务实例对共享资源互斥访问的常用手段。下面从实现原理、基本实现、高级特性及常见问题几个方面详细介绍。

实现原理

Redis 分布式锁的核心原理基于 Redis 的原子操作。Redis 提供了 SETNX(SET if Not eXists)命令,该命令可以在键不存在时设置键值,若键已存在则不做任何操作,返回 0。此外,为避免锁持有者崩溃导致锁无法释放,还需给锁设置过期时间,可使用 EXPIRE 命令。在 Redis 2.6.12 版本之后,可使用 SET 命令的扩展参数将这两个操作合并为一个原子操作。

基本实现

使用 SET 命令实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
"context"
"fmt"
"time"

"github.com/redis/go-redis/v9"
)

func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})

lockKey := "my_distributed_lock"
lockValue := "unique_value"
expireTime := 10 * time.Second

// 尝试获取锁
ctx := context.Background()
result, err := rdb.SetNX(ctx, lockKey, lockValue, expireTime).Result()
if err != nil {
fmt.Println("Error setting lock:", err)
return
}

if result {
fmt.Println("Acquired the lock")
// 模拟业务操作
time.Sleep(5 * time.Second)
// 释放锁
if rdb.Get(ctx, lockKey).Val() == lockValue {
rdb.Del(ctx, lockKey)
fmt.Println("Released the lock")
}
} else {
fmt.Println("Failed to acquire the lock")
}
}

代码解释

  • SetNX 方法尝试设置锁键值,若设置成功则获取到锁。
  • expireTime 为锁的过期时间,防止锁持有者崩溃导致锁无法释放。
  • 释放锁时,先检查锁的值是否与自己设置的值一致,避免误删其他客户端的锁。

高级特性

自动续期

为防止业务执行时间超过锁的过期时间,可使用自动续期机制。Redisson 客户端提供了看门狗(Watchdog)机制实现自动续期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"context"
"fmt"
"time"

"github.com/redis/go-redis/v9"
"github.com/redis/redisson/v3"
"github.com/redis/redisson/v3/config"
)

func main() {
cfg := config.NewSingleServerConfig()
cfg.Address = "redis://localhost:6379"
client := redisson.MustNew(cfg)

lock := client.GetLock("my_distributed_lock")
ctx := context.Background()

// 加锁,不指定过期时间,使用看门狗自动续期
err := lock.Lock(ctx)
if err != nil {
fmt.Println("Error acquiring lock:", err)
return
}
fmt.Println("Acquired the lock")

// 模拟业务操作
time.Sleep(30 * time.Second)

// 释放锁
err = lock.Unlock(ctx)
if err != nil {
fmt.Println("Error releasing lock:", err)
return
}
fmt.Println("Released the lock")

client.Shutdown()
}

可重入锁

可重入锁允许同一个客户端多次获取同一把锁。Redisson 也支持可重入锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"context"
"fmt"
"github.com/redis/redisson/v3"
"github.com/redis/redisson/v3/config"
)

func main() {
cfg := config.NewSingleServerConfig()
cfg.Address = "redis://localhost:6379"
client := redisson.MustNew(cfg)

lock := client.GetLock("my_reentrant_lock")
ctx := context.Background()

// 第一次获取锁
err := lock.Lock(ctx)
if err != nil {
fmt.Println("Error acquiring lock:", err)
return
}
fmt.Println("First acquisition of the lock")

// 再次获取锁
err = lock.Lock(ctx)
if err != nil {
fmt.Println("Error acquiring lock again:", err)
return
}
fmt.Println("Second acquisition of the lock")

// 释放锁两次
err = lock.Unlock(ctx)
if err != nil {
fmt.Println("Error releasing lock:", err)
return
}
err = lock.Unlock(ctx)
if err != nil {
fmt.Println("Error releasing lock:", err)
return
}
fmt.Println("Released the lock")

client.Shutdown()
}

常见问题及解决方案

锁过期问题

业务执行时间超过锁的过期时间,可能导致锁提前释放,引发并发问题。可使用自动续期机制,如 Redisson 的看门狗。

误删锁问题

释放锁时,若不检查锁的值,可能会误删其他客户端的锁。释放锁前需验证锁的值是否与自己设置的值一致。

集群环境下的锁一致性问题

在 Redis 集群环境中,主从切换可能导致锁丢失。可使用 Redlock 算法,该算法通过多个独立的 Redis 节点来保证锁的一致性,但会增加系统复杂度和性能开销。


文章作者: djaigo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 djaigo !
评论
  目录