Skip to content

Commit

Permalink
文章日常更新
Browse files Browse the repository at this point in the history
  • Loading branch information
lizhifuabc committed Nov 1, 2023
1 parent 1894b8a commit 073a89c
Show file tree
Hide file tree
Showing 34 changed files with 868 additions and 5 deletions.
306 changes: 306 additions & 0 deletions docs/database/mysql/MySQL全文索引.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/database/mysql/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# MySQL

[MySQL全文索引](./MySQL全文索引.md)

## MySQL日志

[MySQL日志概述](./MySQL日志.md)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/database/mysql/image/640.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions docs/database/nosql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ Redis等等。
[Redis集群方式](./Redis集群方式.md)

[Redis线程模型](./Redis线程模型.md)

## Redisson

[Redisson看门狗](./Redisson看门狗.md)
39 changes: 39 additions & 0 deletions docs/database/nosql/Redisson看门狗.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Redisson看门狗

**1、互斥性;**

防止多个进程及线程并发访问共享资源,使得资源串行访问操作。

**2、设置锁过期时间;**

为了防止锁悬挂,因为服务宕机,锁不释放问题,其它请求就无法获取锁。

**3、自动续锁超时时间;**

防止业务超时,超过锁过期时间自动释放,打破互斥性。

**4、多条指令需要原子性;**

lua脚本实现多个指令的加锁、解锁及续锁的原子性。

**5、可重入性;**

使用线程ID信息来保证同一线程请求锁的可重入性。

**6、锁误删:自己把别人持有的锁删了;**

多个客户端释放锁,如何防止自己删别人的或者别人删自己申请的锁。

获取锁之前,生成全局唯一id,判断是否是自己的id来避免。

**7、锁等待:发布订阅机制通知等待锁的线程;**

## 看门狗

Redisson加锁的核心代码,本质也是通过redis的lua脚本;

**waitTime:等待时间**;在锁状态变为可持有之前,等待的时间,如果在这个时间内,锁一直被其他客户端线程持有,则放弃等待,返回失败;

**leaseTime:持有时间**;客户端获取到锁之后的最长持有时间,如果设置此参数为10秒,则10秒后,如果此客户端不释放锁,则有redis自行释放,以免造成死锁。

**lockWatchdogTimeout:看门狗时间**;如果客户端不设定持有时间,则持有时间会被设置为看门狗时间,默认为30秒。
8 changes: 6 additions & 2 deletions docs/database/nosql/击穿穿透雪崩.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@

## 缓存穿透

请求数据时,在缓存层和数据库层都没有找到符合条件的数据,因为每次都无法从数据库获取到数据,请求的 key 也就永远不会被缓存,此时会直接将数据库打死。
1. 查询某个Key对应的数据,Redis缓存中没有相应的数据
2. 数据库中也不存在要查询的数据,则数据库会返回空,而Redis也不会缓存这个空结果
3. 造成每次通过这样的Key去查询数据都会直接到数据库中查询,Redis不会缓存空结果。这就造成了缓存穿透的问题。

例如:支付系统中提供一个订单查询接口,由于缓存中没有数据,数据库也没有数据,此时有可能被利用,导致数据库被打死(实际情况可能不会发生,因为数据库也会分库,也会有类似 hmac 的校验)
例如:支付系统中提供一个订单查询接口,由于缓存中没有数据,数据库也没有数据,此时有可能被利用,导致数据库被打死

> 实际情况可能不会发生,因为数据库也会分库,也会有类似 hmac 的校验
![image-20230910171754823](image/image-20230910171754823.png)

Expand Down
6 changes: 6 additions & 0 deletions docs/design/business/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# 业务功能设计

[技术设计模板](./技术设计模板.md)

[权限系统设计](./权限系统设计.md)

[秒杀活动设计](./秒杀活动设计.md)

[二级评论设计](./二级评论设计.md)

# SaaS

[SaaS多租户数据隔离](./SaaS多租户数据隔离.md)
35 changes: 35 additions & 0 deletions docs/design/business/SaaS多租户数据隔离.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# SaaS多租户数据隔离

租户给SaaS平台付租金就能使用平台提供的功能服务。

## 隔离方案

- 一个租户一个数据库,或者将一群租户统一放在一个数据库里

一个租户一个数据库,此时SaaS系统需要连接多个数据库,这种实现方案其实就和分库分表架构设计是一样的,好处就是数据隔离级别高、安全性好,毕竟一个租户单用一个数据库,但是物理硬件成本,维护成本也变高了。

- 共享数据库、不同的数据表:独立的表空间

所有租户共用一个数据库系统,但是每个租户在数据库系统中拥有一个独立的表空间。Schema隔离使用PostgreSQL来实现非常简单,因为他天生支持这种方式,具体可以去PostgreSQL了解。

- 共享数据库、共享数据表:按租户id字段隔离租户(使用较多)

最简单的数据隔离方法,即在每张表中都添加一个用于区分租户的字段(如tenant_id或org_id啥的)来标识每条数据属于哪个租户,当进行查询的时候每条语句都要添加该字段作为过滤条件,其特点是所有租户的数据全都存放在同一个表中,数据的隔离性是最低的,完全是通过字段来区分的,很容易把数据搞串或者误操作。

| 隔离方案 | 成本 | 支持租户数量 | 优点 | 缺点 |
| ---------------- | ---- | ------------ | ---------------------------------------------------------- | ---------------------------------------------- |
| 独立数据库系统 ||| 数据隔离级别高,安全性,可以针对单个租户开发个性化需求 | 数据库独立安装,物理成本和维护成本都比较高 |
| 独立的表空间 || 较多 | 提供了一定程度的逻辑数据隔离,一个数据库系统可支持多个租户 | 数据库管理比较困难,表繁多,同时数据修复稍复杂 |
| 按租户id字段区分 ||| 维护和购置成本最低,每个数据库能够支持的租户数量最多 | 隔离级别最低,安全性也最低 |

## 实现方案

1. mybatis-plus
2. mybatis-flex
3. 基于mybatis拦截器统一处理表的路由

## 其他

如果应用规模进一步扩大,租户数量在持续增加,应用服务器层和数据库层都在持续地水平扩展。但是由于其彼此之间没有任何对应关系,导致所有的应用服务器要与所有的数据库服务器关联(以m 台应用服务器和n 台数据库服务器为例,它们之间的关联有 mxn 个,也是就是每台应用服务器至少要有n个数据库链接 )。

所有的应用服务器不应该是完全平等的,应该与数据库服务器对应。也就是说,不同的租户可能不仅有不同的应用服务器,还有可能有不同的数据库服务器。每组应用服务器都仅连接对应的数据库服务器。
27 changes: 27 additions & 0 deletions docs/design/business/技术设计模板.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 技术设计模板

**模块:**

| 项目名称 | XXX |
| ---------- | ---- |
| 项目负责人 | XXX |
| 模块名称 | XXX |
| 模块负责人 | XXX |

**修订记录:**

| 版本 | 修订人 | 修订内容 | 修订日期 |
| ---- | ------ | -------- | ---------- |
| V1.0 | XXX | XXX | 2022-12-23 |

**需求/背景:**

产品文档或者使用说明:xxxx

**设计目标:**





> https://juejin.cn/post/7184970952298463290
2 changes: 2 additions & 0 deletions docs/design/codedesign/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
[代码命名规范](./一种代码命名规范.md)

[版本号命名规则](./版本号命名规则.md)

[controller规范](./controller规范.md)
8 changes: 8 additions & 0 deletions docs/design/codedesign/controller规范.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Controller规范

## 参数进场检验

1. @NotBlank@NotNull 等注解来进行校验
2. 接口就需要方法上就需要加上@Valid注解

3. 尽量不传递null值。
2 changes: 2 additions & 0 deletions docs/mq/rabbitmq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
[RabbitMQ介绍](./RabbitMQ.md)

[RabbitProperties配置属性解释](./RabbitProperties.md)

[RabbitMQ消息丢失消息重复消息积压](./RabbitMQ消息丢失消息重复消息积压.md)
219 changes: 219 additions & 0 deletions docs/mq/rabbitmq/RabbitMQ消息丢失消息重复消息积压.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# RabbitMQ实战:消息丢失、消息重复、消息积压

RabbitMQ避免消息丢失的方法:

1. 消息确认机制
2. 手动签收机制

## 消息确认机制

主要是生产者使用的机制,用来确认消息是否被成功消费。

```yml
spring:
rabbitmq:
address: 192.168.x.x:xxxx
virtual-host: /
username: guest
password: guest
connection-timeout: 5000
publisher-confirms: true # 消息成功确认
publisher-returns: true # 消息失败确认
template:
mandatory: true # 手动签收机制
```
实现RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback这两个接口的方法后,就可以针对性地进行消息确认的日志记录,之后做进一步的消息发送补偿,以达到接近100%投递的目的。
```java
@Component
@Slf4j
public class RabbitMQSender implements RabbitTemplate.ConfirmCallback,
RabbitTemplate.ReturnCallback {

/**
* 发送消息
*/
public void sendOrder(Order order) {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);

// 发送消息
rabbitTemplate.convertAndSend(xx, xx, order, xx);
}

/**
* 成功接收后的回调
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {

// 如果成功接收了,这里可以对日志表的消息收发状态做更新。
// ....

}

/**
* 失败后的回调
*/
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {

// 如果失败了,这里可以对日志表的消息收发状态做更新,之后通过任务调度去补偿发送。
// ....

}
}
```

## 消息签收机制

RabbitMQ的消息是自动签收的,你可以理解为快递签收了,那么这个快递的状态就从发送变为已签收,唯一的区别是快递公司会对物流轨迹有记录,而MQ签收后就从队列中删除了。

企业级开发中,RabbitMQ我们基本都开启手动签收方式,这样可以有效避免消息的丢失。

前文中已经在生产者开启了手动签收机制,那么作为消费方,也要设置手动签收。

```yml
spring:
rabbitmq:
address: 192.168.x.x:xxxx
virtual-host: /
username: guest
password: guest
connection-timeout: 5000
listener:
simple:
concurrency: 5 # 并发数量
max-concurrency: 10 # 最大并发数量
acknowledge-mode: manual # 开启手动签收
prefetch: 1 # 限制每次只消费一个(一个线程),上面配置5,也就是能一次接收5个
```
消费监听时,手动签收就一行代码,伪代码如下:
```java
@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {

// ....

// 手动签收
channel.basicAck(tag, false);

}
```

## 消息丢失

消息丢失的原因无非有三种:

1. 消息发出后,中途网络故障,服务器没收到;
2. 消息发出后,服务器收到了,还没持久化,服务器宕机;
3. 消息发出后,服务器收到了,消费方还未处理业务逻辑,服务却挂掉了,而消息也自动签收,等于啥也没干。

这三种情况,(1) 和 (2)是由于生产方未开启消息确认机制导致,(3)是由于消费方未开启手动签收机制导致。

解决方案:

1. 生产方发送消息时,要try...catch,在catch中捕获异常,并将MQ发送的关键内容记录到日志表中,日志表中要有消息发送状态,若发送失败,由定时任务定期扫描重发并更新状态;
2. 生产方publisher必须要加入确认回调机制,确认成功发送并签收的消息,如果进入失败回调方法,就修改数据库消息的状态,等待定时任务重发;
3. 消费方要开启手动签收ACK机制,消费成功才将消息移除,失败或因异常情况而尚未处理,就重新入队。

其实这就是前面阐述两个概念时已经讲过的内容,也是接近100%消息投递的企业级方案之一,主要目的就是为了解决消息丢失的问题。

## 消息重复

1. 消息消费成功,事务已提交,签收时结果服务器宕机或网络原因导致签收失败,消息状态会由unack转变为ready,重新发送给其他消费方;
2. 消息消费失败,由于retry重试机制,重新入队又将消息发送出去。

解决方案:

1. 消费方业务接口做好幂等,推荐做法,业务方法幂等这是最直接有效的方式
2. 消息日志表保存MQ发送时的唯一消息ID,消费方可以根据这个唯一ID进行判断避免消息重复;
3. 消费方的Message对象有个getRedelivered()方法返回Boolean,为TRUE就表示重复发送过来的。

## 消息积压

1. 消费方的服务挂掉,导致一直无法消费消息;
2. 消费方的服务节点太少,导致消费能力不足,从而出现积压,这种情况极可能就是生产方的流量过大导致。

解决方案:

1. 既然消费能力不足,那就扩展更多消费节点,提升消费能力;
2. 建立专门的队列消费服务,将消息批量取出并持久化,之后再慢慢消费。

(1)就是最直接的方式,也是消息积压最常用的解决方案,但有些企业考虑到服务器成本压力,会选择第(2)种方案进行迂回,先通过一个独立服务把要消费的消息存起来,比如存到数据库,之后再慢慢处理这些消息即可。

## 总结

1. 消息丢失、消息重复、消息积压三个问题中,实际上主要解决的还是消息丢失,大部分情况下遇不到消息积压的场景,幂等也比较容易实现,所以几乎不存在消息重复的可能;

2. 消息丢失的最常见方案就是定时任务补偿,不论是SOA还是微服务的架构,必然会有分布式任务调度的存在,自然也就成为MQ最直接的补偿方式,如果MQ一定要实现100%投递,这种是最普遍的方案。

实际上不推荐中小企业使用该方案,因为凭空增加维护成本,而且没有一定规模的项目完全没必要,大家都小看了RabbitMQ本身的性能,比如我们公司,支撑一个三甲医院,也就是三台8核16G服务器的集群,上线至今3年毫无压力;

3. 生产者消息确认机制ConfirmCallback和ReturnCallback,这种机制十分降低MQ性能。

可以建立后台管理实现人工补偿,通过识别业务状态判断消费方是否处理了业务逻辑,毕竟这种情况都是少数,性能和运维成本,选择性能;

4. RabbitMQ一定要开启手动签收;

5. 手动签收方式:不论业务逻辑是否处理成功,最终都要将消息手动签收。

举例:

**不科学的用法**:在处理完业务逻辑后再手动签收,否则不签收,就好比客人进店了你得买东西,否则不让走。

```java
@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {

// 处理业务
doBusiness(order);

// 手动签收
channel.basicAck(tag, false);

}
```

**科学的用法**:不论业务逻辑是否处理成功,最终都要将消息手动签收,MQ的使命不是保证客人进店了必须消费,不消费就不让走,而是客人能进来就行,哪怕是随便看看也算任务完成。

```java
@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {

try {
// 处理业务
doBusiness(order);
} catch(Exception ex) {
// 记录日志,通过后台管理或其他方式人工处理失败的业务。
} finally {
// 手动签收
channel.basicAck(tag, false);
}

}
```

可能有人会问你这样不是和自动签收没区别吗,NO,你要知道如果自动签收,出现消息丢失你连记录日志的可能都没有。

另外,为什么一定要这么做,因为MQ是中间件,本身就是辅助工具,就是一个滴滴司机,保证给你送到顺便说个再见就行,没必要还下车给你搬东西。

如果强加给MQ过多压力,只会造成本身业务的畸形。我们使用MQ的目的就是解耦和转发,不再做多余的事情,保证MQ本身是流畅的、职责单一的即可。

本篇主要讲了RabbitMQ的三种常见问题及解决方案,同时分享了一些作者本人工作中使用的心得,我想网上是很难找到的,如果哪一天用到了,不妨再打开看看,也许能避免一些生产环境可能出现的问题。

我总结下来就是三点:

1)、消息100%投递会增加运维成本,中小企业视情况使用,非必要不使用;

2)、消息确认机制影响性能,非必要不使用;

3)、消费者先保证消息能签收,业务处理失败可以人工补偿。

工作中怕的永远不是一个技术不会使用,而是遇到问题不知道有什么解决思路。
Loading

0 comments on commit 073a89c

Please sign in to comment.