趣学架构

https://www.bilibili.com/video/BV1pz4y1j72C

1.大学学点啥

学习能力

  • 知识多
  • 更新换代快
  • 时间约束

image-20240118150913790

2.搭建系统先搭架子

1.用户首页

(用户信息、余额、消费等)

image-20240118152725304

2.新需求

需要添加用户修改功能;存在许多重复代码

image-20240118152826760

解决方案1:添加工具类

image-20240118152927534

模板方法模式

统一逻辑,标准化流程

解决方案2:模板方法模式;保证必须要调用以及调用顺序;通用部分直接一起升级(log、error)

image-20240118152954535

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
import com.google.common.base.Stopwatch;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

public abstract class ServiceTemplate<T, R> {
private final Logger logger = new LoggerImpl();

/**
* 模板统一暴露执行入口
*/
public R process(T request) {
// 1.打印入口日志
logger.info("start invoke, request=" + request);
// 开始计时,用于日志记录耗时
Stopwatch stopwatch = Stopwatch.createStarted();
try {
// 2. 校验参数
validParam(request);
// 3. 子类实现逻辑
R response = doProcess(request);
// 4.打印出口日志
long timeCost = stopwatch.elapsed(TimeUnit.MILLISECONDS);
logger.info("end invoke, response=" + response + ", costTime=" + timeCost);
return response;
} catch (Exception e) {
// 打印异常日志
logger.error("error invoke, exception:" + Arrays.toString(e.getStackTrace()));
return null;
}
}

/**
* 参数校验(交给子类实现)
*/
protected abstract void validParam(T request);

/**
* 执行业务逻辑(交给子类实现)
*/
protected abstract R doProcess(T request);
}
1
2
3
4
5
6
7
8
9
10
// 模拟的Logger类
class LoggerImpl {
public void info(String msg) {
System.out.println("INFO: " + msg);
}

public void error(String msg) {
System.out.println("ERROR: " + msg);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Integer get(Integer userId) {
return new ServiceTemplate<>() {
@Override
protected void validParam(Integer request) {
if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}
}

@Override
protected Integer doProcess(Integer request) {
// 具体业务 获取用户信息、消费情况、余额
return request * request;
}
}.process(userId);

再进一步:如果子需求再次变化,例如添加优惠券信息、消费记录查询限制时间、只有授权才返回余额

这样代码越来越长,并且子业务之间相互影响;耦合

3.搭完架子串珠子

流程引擎

逻辑拆分、边界组装

image-20240118161401411

image-20240118155403367

其实就是实习中遇到的模板引擎玩法

在活动中,就是处理器就是 获取活动信息 过活动人群 过任务人群 过风控 处理业务 (首次进入任务就是添加一次任务记录,然后发一次抽奖机会;抽奖就是消耗抽奖机会,然后执行抽奖流程)

CODE

造珠子(定义组件)

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
public interface Processor {
// 包含两个参数
// 1.入参 本次请求需要的参数,这个也可也定义一个函数来获取
// 2.上下文 处理器之间的信息传递 也是最后结果的返回数据源

boolean needExecute(ProcessRequest request, ProcessContext context); // 灰度
void execute(ProcessRequest request, ProcessContext context);
}

@Component
public class UserInfoQueryProcessor implements Processor {
@Autowired
private UserBaseInfoRepository userBaseInfoRepository;
@Autowired
private UserSpecialInfoRepository userSpecialInfoRepository;

@Override
public boolean needExecute(ProcessRequest request, ProcessContext context) {
// 实现具体的判断逻辑
return true; // 示例,默认总是执行
}

@Override
public void execute(ProcessRequest request, ProcessContext context) {
// 实现具体的处理逻辑
UserBaseInfoVO userBaseInfoVo = userBaseInfoRepository.getUserBaseInfo(request.getUserId());
UserSpecialInfoVO userSpecialInfoVo = userSpecialInfoRepository.getUserSpecialInfo(request.getUserId());

// ... 更新context状态或处理其他业务逻辑
context.setUserBaseInfo(userBaseInfoVo);
context.setUserSpecialInfo(userSpecialInfoVo);
}
}

串珠子(编排组件)

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 引擎接口
public interface ProcessEngine {
void start(ProcessRequest request, ProcessContext context);
}

// 引擎核心
public abstract class AbstractProcessEngineImpl implements ProcessEngine {
@Autowired
private Logger logger;
@Autowired
private ApplicationContext applicationContext;

@Override
public void start(ProcessRequest request, ProcessContext context) {
logger.info("ProcessEngine start, request:" + request);

// 获取执行器列表
List<ProcessNameEnum> processors = getProcessors();
try {
// 逐个运行执行器
for (ProcessNameEnum processorName : processors) {
Object bean = applicationContext.getBean(processorName.getName());
if (!(bean instanceof Processor)) {
logger.error("Processor: " + processorName + " not exist or type is incorrect");
continue;
}

// 执行器开始日志标注
logger.info("Processor: " + processorName + " start");

Processor processor = (Processor) bean;
// 判断执行器是否符合执行条件
if (!processor.needExecute(request, context)) {
logger.info("Processor: " + processorName + " skipped");
continue;
}

// 执行器执行
processor.execute(request, context);

// 执行器结束日志标注
logger.info("Processor: " + processorName + " end");
}
} catch (Exception e) {
// 执行异常中断日志打印
logger.error("ProcessEngine interrupted, exception: " + Arrays.toString(e.getStackTrace()));
// 继续抛出异常
throw e;
}

// 打印引擎执行完成日志
logger.info("ProcessEngine end, context: " + context);
}

protected abstract List<ProcessNameEnum> getProcessors();
}

// 具体引擎
@Component
public class UserInfoQueryProcessEngine extends AbstractProcessEngineImpl {
private static final List<ProcessNameEnum> processorList = new ArrayList<>();

static {
processorList.add(ProcessNameEnum.USER_INFO_QUERY_PROCESSOR);
processorList.add(ProcessNameEnum.MONEY_PROCESSOR);
processorList.add(ProcessNameEnum.CONSUME_RECORD_PROCESSOR);
}

@Override
protected List<ProcessNameEnum> getProcessors() {
return processorList;
}
}

调用(启动流程)

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
public UserInfoDTO getUserInfo(@RequestParam("userId") String userId) {
return (new ServiceTemplate<String, UserInfoDTO>() {
@Override
public void validParam(String request) {
// ... 参数校验逻辑
}

@Override
protected UserInfoDTO doProcess(String request) {
// ... 初始化请求和上下文对象
ProcessRequest processRequest = ProcessRequest.builder().userId(userId).build();
ProcessContext ctx = ProcessContext.builder().build();

// 启动引擎
userInfoQueryProcessEngine.start(processRequest, ctx);

// 从上下文对象中获取数据并填充返回对象
return UserInfoDTO.builder()
.totalMoney(ctx.getTotalMoney())
.maxAmount(ctx.getMaxAmount())
.build();
}
}).process(userId);
}

复杂流程编排

image-20240118162350908

amunda, JBPM, 或Activiti 轻量级框架:LiteFlow

0.JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"initialProcessor": "UserInfoQueryProcessor",
"processMap": {
"UserInfoQueryProcessor": {
"success": "MoneyProcessor",
"failed": "ErrorHandlingProcessor"
},
"MoneyProcessor": {
"success": "ConsumeRecordProcessor",
"failed": "ErrorHandlingProcessor"
},
"ConsumeRecordProcessor": {
"success": null,
"failed": "ErrorHandlingProcessor"
}
}
}

1.返回string,决定着next

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
public interface Processor {
boolean needExecute(ProcessRequest request, ProcessContext context);
String execute(ProcessRequest request, ProcessContext context) throws Exception;
}


@Component
public class UserInfoQueryProcessor implements Processor {
@Autowired
private UserBaseInfoRepository userBaseInfoRepository;
@Autowired
private UserSpecialInfoRepository userSpecialInfoRepository;

@Override
public boolean needExecute(ProcessRequest request, ProcessContext context) {
// 实现具体的判断逻辑
return true; // 示例,默认总是执行
}

@Override
public void execute(ProcessRequest request, ProcessContext context) {
// 实现具体的处理逻辑
UserBaseInfoVO userBaseInfoVo = userBaseInfoRepository.getUserBaseInfo(request.getUserId());
UserSpecialInfoVO userSpecialInfoVo = userSpecialInfoRepository.getUserSpecialInfo(request.getUserId());

// 业务异常返回失败状态
if (userSpecialInfoVo == null){
return "failed"
}

// ... 更新context状态或处理其他业务逻辑
context.setUserBaseInfo(userBaseInfoVo);
context.setUserSpecialInfo(userSpecialInfoVo);
return "success"
}
}

2.编排

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Component
public class ConfigurableProcessEngineImpl implements ProcessEngine {
@Autowired
private Logger logger;
@Autowired
private ApplicationContext applicationContext;

private Map<String, Map<String, String>> processMap;
private String initialProcessorName;

public ConfigurableProcessEngineImpl(String jsonFilePath) {
init(jsonFilePath);
}

private void init(String jsonFilePath) {
// 读取JSON文件并解析为processMap和initialProcessorName
String jsonContent = new String(Files.readAllBytes(Paths.get(jsonFilePath)));
JSONObject jsonConfig = JSON.parseObject(jsonContent);
this.initialProcessorName = jsonConfig.getString("initialProcessor");
this.processMap = parseProcessMap(jsonConfig.getJSONObject("processMap"));
}

@Override
public void start(ProcessRequest request, ProcessContext context) {
String currentProcessorName = this.initialProcessorName;
Processor currentProcessor;

while (currentProcessorName != null) {
currentProcessor = (Processor) applicationContext.getBean(currentProcessorName);
String result;
try {
if (currentProcessor.needExecute(request, context)) {
result = currentProcessor.execute(request, context);
} else {
logger.info("Processor: " + currentProcessorName + " skipped");
result = "Skipped"; // 或者其他表示跳过的结果
}
} catch (Exception e) {
result = "Failed"; // 或者其他表示失败的结果
logger.error("Processor: " + currentProcessorName + " failed, exception: " + Arrays.toString(e.getStackTrace()));
}

currentProcessorName = determineNextProcessor(currentProcessorName, result);
}

logger.info("ProcessEngine end, context: " + context);
}

private String determineNextProcessor(String currentProcessorName, String result) {
Map<String, String> decisionMap = processMap.get(currentProcessorName);
if (decisionMap == null) {
return null;
}
return decisionMap.get(result);
}

// 解析processMap
private Map<String, Map<String, String>> parseProcessMap(JsonObject processMapJson) {
// 实现processMap的解析逻辑
return JSONObject.parseObject(processMapJson.toJSONString(),
new TypeReference<Map<String, Map<String, String>>>() {});
}
}

3.调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected UserInfoDTO doProcess(String request) {
// 创建流程引擎
ConfigurableProcessEngineImpl processEngine = new ConfigurableProcessEngineImpl("/path/to/your/process-flow.json");

// 启动流程引擎
processEngine.start(request, context);


// ... 初始化请求和上下文对象
ProcessRequest processRequest = ProcessRequest.builder().userId(userId).build();
ProcessContext ctx = ProcessContext.builder().build();

// 启动引擎
processEngine.start(processRequest, ctx);

// 从上下文对象中获取数据并填充返回对象
return UserInfoDTO.builder()
.totalMoney(ctx.getTotalMoney())
.maxAmount(ctx.getMaxAmount())
.build();
}

责任链

image-20240118172157467

责任链:沿着这条链传递请求,直到有一个对象处理它为止,具体由哪个对象处理则在运行时动态决定的情况。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
abstract class Handler {
protected Handler successor;

public void setSuccessor(Handler successor) {
this.successor = successor;
}

public abstract void handleRequest(double amount);
}

class NoDiscountHandler extends Handler {
public void handleRequest(double amount) {
System.out.println("No discount applied.");
}
}

class LowDiscountHandler extends Handler {
public void handleRequest(double amount) {
if (amount < 1000) {
System.out.println("Low discount applied. Amount: " + amount);
} else if (successor != null) {
successor.handleRequest(amount);
}
}
}

class HighDiscountHandler extends Handler {
public void handleRequest(double amount) {
if (amount >= 1000) {
System.out.println("High discount applied. Amount: " + amount);
} else if (successor != null) {
successor.handleRequest(amount);
}
}
}

class HandlerChain {
private Handler head;
private Handler tail;

public HandlerChain add(Handler handler) {
if (head == null) {
head = handler;
tail = handler;
} else {
tail.setSuccessor(handler);
tail = handler;
}
return this;
}

public void handleRequest(double amount) {
if (head != null) {
head.handleRequest(amount);
}
}
}

public class ChainDemo {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.add(new LowDiscountHandler())
.add(new HighDiscountHandler())
.add(new NoDiscountHandler());

// Making requests
chain.handleRequest(500);
chain.handleRequest(1500);
}
}

4.系统是个三明治

  • 提高复用性
  • 降低耦合
  • 提高可读性

image-20240118195836506image-20240118195851875

image-20240118200142854

  • [接口层] :对出入参仅做格式上的校验,不能涉及“例如用户是否在黑名单中”这样的校验。
  • [服务层] :负责编排流程、处理rpc请求、控制同异步。不能涉及领域概念。
  • [领域层] :针对领域规则来实现具体的能力。
  • [数据层] :仅对数据做CRUD,不能涉及对数据的额外加工。

image-20240119160910358

5.DDD

复杂度提升

  • 满足基本需求
  • 良好的扩展性
  • 稳定性、性能

image-20240119171329282

设计策略:

  • 搞清功能
  • 分析建模
  • 便于拆分

case:卖家可以在网上挂商品售卖,买家可以选择商品并购买,购买后卖家会发快递,买家收到货后确认收货,网站把款项结算给卖家。

image-20240119172529042

战略设计

解释业务,建立业务模型,划分业务边界

事件风暴

image-20240119165000678

事件:行为的结果(业务的重点),再通过事件反推整个流程

image-20240119164946344

领域建模

分析领域模型
  • 找出事件风暴中的名词

  • 连接名词

    image-20240119172209854
找聚合

直接关系最多的节点

image-20240119172306137

划分(限界上下文)
image-20240119172428922

1.整理出了重要的业务概念和规则.
2.所有角色都对概念对齐了认知
3.识别了重要的领域模型,继而指导了系统模型
4.做了系统划分

丛业务嘴里的模糊描述 ->清晰的业务概念(多方认知)具体系统建设的内容

战术设计

image-20240119173515013

领域模型:提供了基本的能力,包含业务规则(如类文件对外暴露的方法)

领域服务:就是命令(动作),由多个领域模型聚合

image-20240119173056903

应用层:编排领域服务;同时需要处理消息(例如下单后的邮件通知通过事件驱动实现)

image-20240119173900690

目录结构:

image-20240119174138306

6.0铁三角

image-20240120125313890

6.还得是设计模式(扩展-功能扩展)

  • 功能扩展
  • 流量扩展

看不懂 改不动 风险高

会写代码的人很多,写好的代码的人少

image-20240120125646682

case

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
/**
* 转账服务
* @param payer 付款方
* @param payee 收款方
* @param money 转账金额
* @return 是否转账成功
**/
public boolean transfer(String payer, String payee, String money) {
Log.info("transfer start, payer={}, payee={}, money={}", payer, payee, money);

// 1.检查参数
if (!isValidUser(payer) || !isValidUser(payee) || !isValidMoney(money)) {
return false;
}

// 2.调用转账服务
TransferResult transferResult = transferService.transfer(payer, payee, money);
if (!transferResult.isSuccess()) {
return false;
}

// 3.查询用户通知方式
UserInfo userInfo = userInfoService.getUserInfo(payee);
if (userInfo.getNotifyType() == NotifyTypeEnum.SMS) {
// smsNotifyService是第三方jar包
smsClient.sendSms(payee, NOTIFY_CONTENT);
} else if (userInfo.getNotifyType() == NotifyTypeEnum.MAIL) {
// mailNotifyService是第三方jar包
mailClient.sendMail(payee, NOTIFY_CONTENT);
}

// 记录转账账单(发送事件给转账系统)
billService.sendBill(transferResult);
// 转账监控打点(调用监控jdk)
monitorService.sendRecord(transferResult);
// 记录转账额度(调用额度中心)
quotaService.recordQuota(transferResult);

Log.info("transfer success");
return true;
}
  1. 入参出参不具备扩展性,可以考虑使用对象参数

  2. 参数的校验可以使用责任链优化,所有校验方法都添加到一个list中 context.getBeansOfTypeParamValidator.class)

    image-20240120150550932

  3. 通知方式,使用多态替换条件表达式 策略模式或适配器;和上面最大区别在于只执行一个而不是都执行 所以要一个一个添加

    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
    @Service
    public class NotifyServiceManager implements InitializingBean {
    @Autowired
    private SmsNotifyService smsNotifyService;

    @Autowired
    private MailNotifyService mailNotifyService;

    private final Map<NotifyTypeEnum, NotifyService> notifyServiceMap = new HashMap<>();

    @Override
    public void afterPropertiesSet() throws Exception {
    // 注册通知类型到通知服务的映射关系
    notifyServiceMap.put(NotifyTypeEnum.SMS, smsNotifyService);
    notifyServiceMap.put(NotifyTypeEnum.MAIL, mailNotifyService);
    }

    public void notify(NotifyTypeEnum notifyTypeEnum, String userId, String content) {
    NotifyService notifyService = notifyServiceMap.get(notifyTypeEnum);
    if (notifyService == null) {
    throw new RuntimeException("Notify service not exist for type: " + notifyTypeEnum);
    }
    notifyService.notifyMessage(userId, content);
    }
    }
  4. 最后非主链路的统计、打点,使用观察者模式实现;并结合线程池加速及错误隔离;和校验器区别在于这是非主链路

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public interface TransferObserver {
    void update(TransferResult transferResult);
    }
    public class BillServiceObserver implements TransferObserver {
    @Override
    public void update(TransferResult transferResult) {
    billService.sendBill(transferResult);
    }
    }

    public class MonitorServiceObserver implements TransferObserver {
    @Override
    public void update(TransferResult transferResult) {
    monitorService.sendRecord(transferResult);
    }
    }
    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
    @Component
    public class TransferSubject implements InitializingBean {
    @Autowired
    private ApplicationContext applicationContext;
    private final List<TransferObserver> transferObserverList = new ArrayList<>();
    // 异步线程池
    private final ExecutorService executorService = Executors.newFixedThreadPool(10);

    @Override
    public void afterPropertiesSet() throws Exception {
    Map<String, TransferObserver> transferObserverMap = applicationContext.getBeansOfType(TransferObserver.class);
    transferObserverMap.values().forEach(this::addObserver);
    }

    /**
    * 触发观察者
    */
    public void notifyObservers(TransferResult transferResult) {
    transferObserverList.forEach(transferObserver -> {
    // 异步执行
    executorService.execute(() -> transferObserver.update(transferResult));
    });
    }

    /**
    * 添加观察者
    */
    public void addObserver(TransferObserver transferObserver) {
    transferObserverList.add(transferObserver);
    }
    }

至此新增参数校验、通知类型、后处理等,transfer方法不用修改

设计原则

image-20240120153221074

7.没有扛不住的流量(扩展-流量扩展)

引入

可能存在的问题:

image-20240120180511652

此外,还需要拦截恶意的流量,并在特殊情况下对部分有效流量进行降级

image-20240120180655965

水平扩展

  • 流量的路由(负载均衡,直接平均或者按照机器性能,实际上机器同样,所以直接平均,降低负载均衡时的复杂度)

    1. DNS
    2. NGINX 没有自动服务发现的能力
    3. Eueka Nacos Consul

    image-20240120181702521

  • 服务器间的数据共享问题(实现无状态)

    1. 最简单的方式:复制或者拆分;;引入额外的效率 复杂度 一致性问题

    2. 数据中心化:缓存以及磁盘都放到中心化的服务上

      image-20240120182017970

垂直拆分

image-20240120182133557

也就是微服务拆分,使得服务间不受到影响,隔离风险;灵活配置合理分配资源

单元化部署

通常情况下前两种够了,但:

image-20240120183320165

根据用户的id,在服务上以及数据(sharding)上都进行拆分

image-20240120183309123

image-20240120183359206

image-20240120184018167

8.读的慢有妙招(性能)

后台服务高性能设计之道

性能:

  • 读性能
  • 写性能

image-20240120192800027

缓存

使用层面

image-20240120185031580

本地 vs 中心

image-20240120185122795

甚至于同时使用多级缓存

一致性问题

  1. 添加过期时间
  2. 先更新DB再删除缓存cache aside pattern(小概率:B来的时候没有缓存,B读取数据库,A更新数据并删除redis,B写脏数据到redis)
    • 更新DB再更新缓存、更新缓存再更新db
      1. 同时更新时顺序问题
      2. 多次更新时重复无效的更新
      3. 此外更新缓存再更新db,如果更新db失败,缓存不好回滚
    • 删除缓存再更新db:A删完缓存来了查询B,B查询完成后写入脏数据到redis
  3. 延时双删:先更新DB再删除缓存,再异步删除 (实际上网上资料都是先删除缓存再更新DB,再异步删除)
  4. 缓存永不过期,并且周期性全量刷新

读写分离

DB的访问做读写分离,写在主,binlog等方式同步到从

一致性问题

  • 大部分时间忽略

  • 前端直接短暂延时

  • 强一致场景强制读主

  • 路由标记,引入了一个标记(过期时间大于同步时间)作为同步标记,会导致多一次读缓存,巧妙但不推荐

    image-20240120191525962

并发

一个通用的思路,针对性能问题通用解决方案

image-20240120191900959

异步

image-20240120192137793

其实就是之前提到的转载后的打点使用异步线程池实现,主线程直接返回

产品设计

  • 分页
  • 递进展示
  • 降低极致的准确性要求,允许短暂的不一致
  • 峰值流量降级非重要功能
  • 控制主动(点击重试)或被动(超时重试)重试

其他 优化协议、流量拦截、静态缓存、数据压缩等等

9.写性能难提升(性能)

为什么难?

  1. 写的丢失代价
  2. 必须要磁盘(可靠性场景),而读可以是缓存
  3. 写时常需要加锁
  4. 资损

选择数据库

image-20240121204714055

合理加锁

image-20240121205243774

异步

image-20240121222045378

优化方案的轮询是查询缓存的,放置数据库压力过大

TODO:添加一个缓存标记,可以用在判题请求中,这样轮询时就不用查询数据库

批量插入

  • db批量操作快、减少加锁释放锁时间

  • 缺点:更新存在延时image-20240121222926648

  • 方案二(缓冲记账)引入了流水数据库表,实现持久化了数据但不用加锁

文件

文件系统的写入通常比数据库写入要快,之后再把文件同步到db,同步时可以使用拆分思想并发处理

image-20240121223503832

缓存

并不需要百分百正确,缓存挂了就捞取redis自带的持久化数据,或者自己定时任务捞取缓存持久化到数据库

image-20240121223646419

总结

image-20240121224327937

10.稳定性引入

image-20240121225347286

image-20240121225544326

核心服务4个9:52.6 mins 一年不可用时间

image-20240121231622560

image-20240121231642209

image-20240121231724254

总结引入

image-20240121232021199

11.稳定性之设计时

幂等

  1. 请求携带唯一ID(可以前端生成也可也后端生成返回)

  2. 后端流水数据库唯一ID key,业务前需要先落库流水数据库实现幂等

  3. 更进一步,添加分布式锁

    image-20240122122058300

隔离

image-20240122123053045

降级

避免被下游影响;强依赖变成弱依赖,账单服务失败不能影响查询余额接口;

image-20240122123255388

  1. 前端直接拆分成两个请求
  2. 直接try catch,这里还是会对下游发起请求,如果下游返回时间比较长,还需要等待
  3. 通过配置中心(一般会缓存到本地,配置变更再推送),根据配置决定是否请求

流控

避免被上游影响

  • 限制流量
  • 控制速度

image-20240122123755636

一致性

image-20240122124816983

兼容性

旧接口进行了改造,字段发生了变化

image-20240122124959078

标准方法:后端逻辑需要兼容旧逻辑,前端字段冗余,新旧逻辑都要传递;之后定时清理

因此在该变更场景中,实际上是先新增一个字段,再清理时删除原字段

打日志

image-20240122130621494

  • 可以接受的错误,存在降级策略:warn
  • 预期之外、会终端流程:error
  • 大厂最佳实践通常是异步打印日志:磁盘 -> 发送到队列,由别的线程负责

image-20240124152012901

image-20240124154346120

摘要日志

可以结合工具做统计,可视化等

image-20240124154157858

12.稳定性之变更时

升级产品功能、修复产品缺陷;80%故障

  • 代码发布
  • 配置变更
  • 数据库修改
  • 库表变化

测试

image-20240122132115071

兼容性和前面说的类似,下游同时兼容上游新旧代码 a调用b:a旧-b新、a新-b新、甚至于a新-b旧

对比流量

流量复制

预发布环境DB、Cache相同,因此只能针对读服务

  • 方法1. 线上在业务执行完成后转发到预发布环境,存在入侵
  • 方法2. 在网关同时转发到两个环境

image-20240122133710703

线下环境流量回放

  • 将请求、RPC、中间件、DB都录制(框架实现线上录制功能,如果让你设计这个框架,如何实现?)
  • 回放时全部mock掉

image-20240122134112992

发布顺序

image-20240122134845701

容量评估(压测)

唯一方法:压测

image-20240122135140022

可监控

埋点、日志、异常、机器指标、成功率、RT

image-20240122135710503

可灰度

变更逐步生效

实现灰度:用户ID后两位

image-20240122140351961

陷阱

开关变量实现灰度,多次请求中途变更了开关

image-20240122140557705

image-20240122140708294

可回滚

回滚不难,如何快速回滚,并且有时候存在新数据了,旧代码是否能够兼容或者数据回滚

image-20240122141222095

image-20240122141402267

13.稳定性之运行时

跑着跑着出错了;真正故障时能做的事情很少,重点需要前期准备好

监控+报警

包含日志的采集 & 报警

image-20240122163959159

主动探测

主动模拟请求,周期性执行

image-20240122164316168

对账系统

美团配送资金安全治理之对账体系建设 - 美团技术团队 (meituan.com)

image-20240122164445756

实时

  • 植入到链路中,失败立马进行拦截
  • 影响核心链路性能

image-20240122164531241

准实时

  • 通过异步发送消息或者监听binlog

image-20240122164731540

离线

定时拉取数据并做校验

image-20240122164920770

分布式trance

还原链路的调用关系

image-20240122165049137

14.错误处理显真功(细节)

某些因素导致流程没有按照预期执行完成

image-20240123151905647

引入

  • 入参校验(电话、金额)
  • 中间用户信息结果判断
  • 业务返回判断
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
50
51
52
53
54
55
56
57
58
59
60
61
62
public TransferResponse transfer(Transferparam transferparam) {
// 1.查询用户信息并校验
UserInfoResult payerUserInfoResult = userInfoService.queryUserInfo(transferParam.getPayerPhoneNo());
UserInfoResult payeeUserInfoResult = userInfoService.queryUserInfo(transferParam.getPayeePhoneNo());

// 2.转账
TransferRequest transferRequest = TransferRequest.builder()
.payerId(payerUserInfoResult.getData().getUserId())
.payeeId(payeeUserInfoResult.getData().getUserId())
.money(transferParam.getMoney())
.build();
transferService.transfer(transferRequest);

// 3. 记录额度
quatoService.recordQuato(payerUserInfoResult.getData().getUserId(), transferParam.getMoney());

return TransferResponse.builder().retCode(SUCCESS_CODE).build();
}

public TransferResponse transfer(Transferparam transferparam) {
// 输入参数校验
String payerPhoneNo = transferParam.getPayerPhoneNo();
String payeePhoneNo = transferParam.getPayeePhoneNo();
BigDecimal money = transferParam.getMoney();

if (!isValidPhoneNo(payerPhoneNo) || !isValidPhoneNo(payeePhoneNo)) {
throw new IllegalArgumentException("Invalid phone number");
}

if (money == null || money.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Invalid transfer amount");
}

// 1.查询用户信息并校验
UserInfoResult payerUserInfoResult = userInfoService.queryUserInfo(payerPhoneNo);
UserInfoResult payeeUserInfoResult = userInfoService.queryUserInfo(payeePhoneNo);

if (payerUserInfoResult == null || payerUserInfoResult.getData() == null || payeeUserInfoResult == null || payeeUserInfoResult.getData() == null) {
throw new IllegalArgumentException("Invalid user information");
}

// 2.转账
TransferRequest transferRequest = TransferRequest.builder()
.payerId(payerUserInfoResult.getData().getUserId())
.payeeId(payeeUserInfoResult.getData().getUserId())
.money(money)
.build();
boolean transferResult = transferService.transfer(transferRequest);

if (!transferResult) {
throw new RuntimeException("Transfer failed");
}

// 3. 记录额度
boolean recordResult = quatoService.recordQuato(payerUserInfoResult.getData().getUserId(), money);

if (!recordResult) {
throw new RuntimeException("Failed to record quota");
}

return TransferResponse.builder().retCode(SUCCESS_CODE).build();
}

错误处理方式

  • 返回错误码 可以包含更加复杂的信息;无性能损耗
  • 抛出异常 写起来简单 (目前主流rpc都支持异常传递)
  • 都无法用于异步场景!

image-20240123144451520

推荐:系统内使用中断,PRC接口交互错误码

异步异常

image-20240123145901501

错误码设计

image-20240123152917587

这里是代码写死,也可也配置中心配置

异常设计方式

CommonException+枚举

image-20240123152134856

各种不同异常+上面:可以通过异常类型进行不同处理(降级等)

image-20240123152341699

异常映射错误码

由于内部是使用异常,返回给上游是状态码,最后需要catch将异常转换为外部状态码

image-20240123153212173

  • 不要吃掉异常,一定要打日志

  • 调用方不要感知错误码,否则沟通更新成本高,需要和全部上游对齐

    image-20240123153725656

15.打日志是技术活

打日志

16.技术文档

目的:

  • 确保方案可行
  • 提早识别风险
  • 对齐系统修改
  • 评估工时投入

金币提现场景

https://www.yuque.com/codingbetterlife/lession/pka2nhb3yqoiqbhl?singleDoc
密码:wsg3

功能描述

功能点:用户提现金币到银行卡,有每天的限额

image-20240125134907012

  • 页面展示:总金币、可提现金币、额度、银行卡
  • 准备提现:输入金额,并调取后端查询收费
  • 确认提现:扣减金币到银行卡

核心流程

image-20240125134931139

其中具体的每一个具体服务使用流程引擎实现

image-20240125135035515