基础架构理论

mysql分片

分片算法主流的两种为范围法分片法

缓存

CDN缓存原理

Nginx缓存设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http {
...
# 用于缓存的本地磁盘目录是 /path/to/cache/
# 设置了一个两级层次结构的目录
# inactive=60m 某个缓存超过60分钟没有被访问则自动删除
# max_size文件最大为10g
# keys_zone 命名一个空间为my_cache的缓存,配置10M的内存空间(差不多可以存储8)
proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

server {
proxy_cache mycache;
location / {
proxy_pass http://localhost:8000;
}
}
}

MySQL主键问题

主键自增

  1. 不分片无集群的情况下,主键自增是可行的
  2. 当分片时,主键自增只能采用”范围分片”形式,会产生“尾部热点”效应,无法分担数据库压力,例如数据库2的范围分布在1w-2w数据之间,连续的自增主键数据某一时段内只能存放在数据库2

UUID做主键不可取的原因

UUID是无序的,作为主键会涉及大量索引重排

雪花算法

分布式ID推荐雪花算法,格式如下

注意:时间回拨可能会影响ID唯一性,例如服务器时间手动回调或者多个服务器之间时间同步

MySQL脏读、幻读、不可重复读

  • 脏读指读取到其他事务正在处理的未提交数据
  • 不可重复读指并发更新时,另一个事务前后查询相同数据时的数据不符合预期
  • 幻读指并发新增、删除这种会产生数量变化的操作时,另一个事务前后查询相同数据时的不符合预期

MySQL默认事务级别是Repeatable Read(可重复读),MySQL 5.1以后默认存储引擎就是InnoDB,因此MySQL默认RR也能解决幻读问题

MySQL MVCC(多版本并发控制)机制

一些基础概念

  • 快照读就是最普通的Select查询SQL语句
  • 当前读指代执行下列语句时进行数据读取的方式(Insert、Update、Delete、Select…for update、Select…lock in share mode)
  • ReadView是一个数据结构,包含4个字段
    • m_ids:当前活跃的事务编号集合
    • min_trx_id:最小活跃事务编号
    • max_trx_id:预分配事务编号,当前最大事务编号+1
    • creator_trx_id:ReadView创建者的事务编号

RC读已提交模式:在每一次执行快照读时生成ReadView

RR可重复读:仅在第一次执行快照读时生成ReadView,后续快照读复用(有例外)

例外:当两次快照读之间存在当前读,ReadView会重新生成,导致产生幻读

MySQL索引选择

  1. 搜索严禁左模糊和全模糊
  2. 并不是所有右模糊都能用到索引,要看索引命中率,高命中率还是会采用全表扫描
  3. 使用force index(索引名称)强制使用索引

MySQL语句对于分页的优化

语句

select * from A order by creat_time limit 500000,10

分析

如果只是给creat_time字段添加索引是没用的(非主键索引的叶子节点保存了主键的值,在用非主键索引查询时,先会查询出主键的值,然后在主键索引的表中查询),所以对于非主键索引查询应该select creat_time from A order by creat_time limit 500000,10

解决方法

select * from A where creat_time >= (select creat_time from A order by creat_time limit 500000,1) order by creat_time limit 10

存在的问题

如果并发量很大的前提下,例如这一页的数据中第一行creat_time和最后一行creat_time是相同的,那么就会进入死循环,解决方法是数据增加偏移量处理

通过业务上进行处理

当数据量庞大的情况下,根据业务就行处理,没有必要将所有数据全部显示出来(比如几千页后的数据)用户不会关心后面的数据,通过控制数据的总量来实现查询速度加快

布隆过滤器

为了防止恶意缓存穿透攻击,使用布隆过滤器拦截无效请求

  1. 初始化过滤器,例如将商品表中的所有商品编号通过多次hash的方式让布隆过滤器进行标记
  2. 过滤器是会出现误判的,降低误判的方式有增加二进制数组位数增加Hash次数
  3. 过滤器hash位置任意一个不存在则一定不存在,hash位置都存在则可能存在(存在误判几率,一般设置为1%)

数据删除存在的问题

如果库中数据删除,是无法直接删除布隆过滤器针对该数据存储的位置的,因为同一个位置会被多个数据引用,针对删除有两种办法

1. 异步定时重构布隆过滤器
1. 计数布隆过滤器代替普通布隆过滤器,其原理是每一个位上都额外增加了一个计数器,在插入元素时给对应的 k (k 为哈希函数个数)个 Counter 的值分别加 1,删除元素时给对应的 k 个 Counter 的值分别减 1

CAP定理

  • 一致性C:更新操作成功后,所有节点在同一时间的数据完全一致
  • 可用性A:用户访问数据时,系统是否能在正常响应时间返回预期的结果
  • 分区容错性P:分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务

三者只能满足其二,例如

  • AP表现为订单创建后不等待库存减少直接返回处理结果(为了追求客户体验,电商一般采用这个方法)
  • AC表现为不再拆分数据系统,在一个数据库的一个事务中完成操作,也就是单体应用(小应用)
  • CP表现为订单创建后一直等待库存减少后才返回结果

如何保证最终一致性

  • 重试:例如MQ就是通过重试的方案来尽可能保证数据不丢失
  • 数据校对程序:通过定时任务,每隔几分钟去抽取、补发数据
  • 人工介入

canal

Canal主要功能是监听mysql的binlog日志,进行解析后将消息传入队列,订阅队列的应用获取推送

Redis高可用架构

redis主从一致性原理

Redis Sentinel(哨兵模式)

针对redis的侦察选举方式

针对Sentinel自我本身的选举方式

Redis Cluster(集群模式)

集群数据分散分布算法

Redis Cluster 集群采用Hash Slot(哈希槽)分配,Redis集群预分好16384个槽,初始化集群时平均规划给每一台Redis Master

假设目前有三个主节点,那么将会把16384平分为3个区域,通过crc16算法对key进行计算得到一个数值,再对16384进行取余,得到的值放入对应范围内的区域即可,反之亦然

集群配置文件redis-cluster.conf

创建集群命令

1
2
3
4
5
6
7
# --cluster-replicas 1 代表一个主节点对应一个副节点
# redis默认前面的是主节点102,103,104,后面的是对应的副节点110,111,112
# -a 123456 redis密码
./src/redis-cli -a 123456 --cluster create
192.168.31.102:6379 192.168.31.103:6379 192.168.31.104:6379
192.168.31.110:6379 192.168.31.111:6379 192.168.31.112:6379
--cluster-replicas 1

集群模式和哨兵模式的区别

哨兵模式

每个节点持有全量数据,且数据保持一致,为系统Redis高可用

集群模式

每个节点主数据不同,是数据的子集,利用多台服务器构建集群提高超大规模数据处理能力,同时提供高可用支持

缓存的设计模式

  • Cache Aside Pattern: 更新操作时,先写库,再删除缓存(不要更新缓存,并发数据易出错)(并不能数据保证强一致性

数据出现不一致的场景

延迟双删

图二中数据不一致主要原因是写缓存在删缓存之后,简而言之只要保证保证删缓存一定在写缓存后即可。可添加定时任务或mq解决,时间间隔需要以线程2的逻辑处理时间作为参考

  • 如要保证缓存与数据库强一致性,最好使用分布式锁,但那样并发性极低

分布式事务Seata框架

通常分布式事务流程

事务流程分为两个阶段(二阶段提交)

  1. 事务协调者发起请求,通知各个服务处理本地事务,所有服务处理各自的 逻辑,处理完成后事务不提交
  2. 事务协调者判断是否全部服务都正常进行,如果正常则再下达指令让各个服务事务提交,如任意一个出现异常,全员回滚

手动事务提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Resource
private PlatformTransactionManager platformTransactionManager;
@Resource
private TransactionDefinit ion transactionDefinition;

public void test(HttpServletRequest request) {
TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
try {
User u = new User();
u.setNickName("nickName");
userMapper.insert(u);
platformTransactionManager.commit(transactionStatus);
} catch (Exception e) {
platformTransactionManager.rollback(transactionStatus);
}
}

自动提交事务

1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableTransactionManagement
public class MinioApplication {
public static void main(String[] args) {
SpringApplication.run(MinioApplication.class, args);
}
}

@Transactional(rollbackFor = Exception.class)
public void test(HttpServletRequest request) {
User u = new User();
u.setNickName("nickName");
userMapper.insert(u);
}

Seata分布式事务流程

Seata和通常流程的区别

每个服务逻辑完成之后先提交事务不用等事务协调者的二次命令

SeataAT模式(默认)事务回滚

  1. 在所有数据库中添加UNDO_LOG表(回滚日志表)
  2. 底层使用sqlparse对事务执行的sql进行解析,生成相反的sql插入UNDO_LOG表中 (事务执行insert,它就生成delete)
  3. 事务协调者下达提交命令,删除UNDO_LOG表中该条数据即可。下达回滚命令,执行UNDO_LOG表中逆向sql,还原数据

Seata避免高并发脏读脏写

因为Seata第一阶段本地事务就提交了,所以可能出现问题是同一时刻其他事务可能会造成脏读脏写

保障接口幂等性方法

  1. 在原有的代码中添加前置判断,if(!员工已调薪){进行调薪},虽然方便但是容易遗漏
  2. 构建幂等表

  • 应用系统发送的请求头需要强制带上一串能保证一段时间内唯一的字符
  • 应用网关通过nginx+lua脚本从redis中查看是否存在携带唯一性字符的请求
  • 如果存在,则直接过滤请求返回错误码,如果不存在,则请求下达数据服务,进行一个正常的请求逻辑

并发数据冲突解决

悲观锁

在查询的时候,使用for update产生行级锁,例如select * from XX where id = 1 for update,此时只有一个线程可以读取id为1的数据(其他id数据可以获取),其他线程因为拿不到行级锁,只能进行等待(效率低)

乐观锁

数据库中新增一个_version字段来控制数据版本

1
2
select money,_version from acc where id = 1001;
update acc set money = 900,_version = _version + 1 where id = 1001 and _version = _version;

遇到冲突后的解决方法

- 前端应用提示,请稍后重新尝试
- 附加`Spring-retry`进行方法重试`@Retryable(value={VersionException.class},maxAttempts=3)`

JWT认证方案设计

JWT = 标头Header(加密方式) + 载荷Payload(需要传递的参数) + 签名Sign

  1. 网关统一校验

  1. 应用认证

方案一:JWT校验无感知,验签过程无侵入,执行效率低,适用于低并发企业级应用
方案二:控制更加灵活,有一定代码侵入(自定义注解,AOP注入即可),代码可以灵活控制,适用于追求性能互联网应用

JWT续签方案设计

  • 不改变令牌续签 (通过添加redis来控制jwt的过期时间)

注意的是为了更加的安全,redis的key不建议使用jwt,而是把环境特征和数据进行md5加密作为key,redis只是用来标记当前环境登陆信息jwt的一个有效时间,并不存储jwt

  • 允许改变令牌续签

重复生成jwt问题

认证中心设计一个计时Map数据结构,只记录过去n秒内的原始jwt刷新所生成新jwt数据,几秒内如果发现同样的jwt在再次请求刷新,就返回相同的新jwt数据

Nginx高可用

  1. 图上左右两个框内的Ng实际是同一个模型,为了方便观察将它区分(上图共用了2台Ng)
  2. Ng之间的检查心跳使用VIP(虚拟ip)技术
  3. Ng的轮询依赖于DNS-Server

Docker中NG负载均衡配置

  1. 给需要用到的应用实例创建一个统一的默认网段docker network create default_network
  2. docker run --name app1 --network default_network -p 8080:8080 docker run --name app2 --network default_network -p 8081:8080
  3. docker run --name nginx -v /XX/nginx/nginx.conf:/etc/nginx/nginx.conf --network default_network -p 80:80 -d nginx
  4. Nginx.conf配置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http {
....... # 省略其他配置
upstream XX{
server app1:8080
server app2:8080
}
server {
listen 80;
location /{
proxy_pass http://XX
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

MQ可靠性投递实现

可能出现的问题以及如何避免

  1. 异步刷盘,改同步刷盘(即2、3过程)
  2. 存储介质损坏,建议采用RAID10或分布式存储
  3. 不要启用自动Ack,应当手动ack处理确保消费者正常消费信息

RabbitMQ六种队列模式

1
2
3
4
5
6
7
8
9
10
11
12
1. 简单模式
生产者 -> 队列 -> 消费者
2. 工作队列
生产者 -> 队列 -> 多个消费者 (类似于负载均衡的功能)
3. 发布订阅 (Fanout)
生产者 -> 交换机 -> 多个队列 -> 多个消费者 (所有消费者拿到的消息完全相同)
4. 路由模式 (Direct)
生产者 -> 交换机 -> (根据路由规则)-> 多个队列 -> 多个消费者 (不同消费者只能拿到符合自己规则路由的消息)
5. 主题模式 (Topic)
和路由模式类似,只不过主题模式是模糊匹配,(例如*.orange.*,lazy.#)通配符方式去匹配路由
6. RPC同步通信模式
前面的模式,生产者发送消息后即脱离整个流程,但是RPC模式需要全程参与,最后接收消费者发来的反馈信息

RabbitMQ解决消息堆积

Springboot配置死信队列

1
spring.rabbitmq.listener.simple.default-requeue-rejected=false
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
@Configuration
public class Config {

public static final String BUSINESS_EXCHANGE_NAME = "dead.letter.demo.simple.business.exchange";
public static final String BUSINESS_QUEUEA_NAME = "dead.letter.demo.simple.business.queuea";
public static final String DEAD_LETTER_EXCHANGE = "dead.letter.demo.simple.deadletter.exchange";
public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "dead.letter.demo.simple.deadletter.queuea.routingkey";
public static final String DEAD_LETTER_QUEUEA_NAME = "dead.letter.demo.simple.deadletter.queuea";

// 声明业务Exchange
@Bean("businessExchange")
public FanoutExchange businessExchange() {
return new FanoutExchange(BUSINESS_EXCHANGE_NAME);
}

// 声明死信Exchange
@Bean("deadLetterExchange")
public DirectExchange deadLetterExchange() {
return new DirectExchange(DEAD_LETTER_EXCHANGE);
}

// 声明业务队列A
@Bean("businessQueueA")
public Queue businessQueueA() {
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);
return QueueBuilder.durable(BUSINESS_QUEUEA_NAME).withArguments(args).build();
}

// 声明死信队列A
@Bean("deadLetterQueueA")
public Queue deadLetterQueueA() {
return new Queue(DEAD_LETTER_QUEUEA_NAME);
}

// 声明业务队列A绑定关系
@Bean
public Binding businessBindingA(@Qualifier("businessQueueA") Queue queue,
@Qualifier("businessExchange") FanoutExchange exchange) {
return BindingBuilder.bind(queue).to(exchange);
}

// 声明死信队列A绑定关系
@Bean
public Binding deadLetterBindingA(@Qualifier("deadLetterQueueA") Queue queue,
@Qualifier("deadLetterExchange") DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEA_ROUTING_KEY);
}

}
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
@RestController
public class ConsumerController {

@RabbitListener(queues = Config.BUSINESS_QUEUEA_NAME)
public void messageReceiveA(@Payload Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
System.out.println("收到业务消息A:" + message);
// 签收失败 requeue 被拒绝的是否重新入队列
channel.basicNack(tag, false, false);
}

/**
* 死信队列
*/
@RabbitListener(queues = Config.BUSINESS_QUEUEA_NAME)
public void receiveA(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
System.out.println("收到死信消息A:" + message);
channel.basicAck(tag, false);
}
}

@RestController
public class ProviderController {
@Autowired
private RabbitTemplate rabbitTemplate;
public static final String BUSINESS_EXCHANGE_NAME = "dead.letter.demo.simple.business.exchange";

@PostMapping("/sendOrder")
public void sendOrder(Message message) {
//CorrelationData对象的作用是作为消息的附加信息传递,通常我们用它来保存消息的自定义id
CorrelationData correlationData = new CorrelationData();
correlationData.setId(message.getMessageId());
// convertAndSend方法参数为交换机名称,routingkey,传输的信息,消息唯一ID
rabbitTemplate.convertAndSend(BUSINESS_EXCHANGE_NAME, "", message, correlationData);
}
}

MQ中pull模式和push模式如何取舍

写扩散优化

  • 设置上限,比如粉丝好友上限为5000个
  • 优化存储策略,采用NoSQL或大数据方案
  • 限流策略,X分钟内完成消息发布

读扩散优化

  • MQ削峰填谷,超长队列直接拒绝
  • 增加轮询间隔,减少请求次数
  • 服务端增加缓存,优化查询效率
  • 增加验证码,分散时间,减少机器人访问

混合模式

订阅数小于某一个值采用Push模式,大于某一个值采用Pull模式,这个值需要根据服务器压力进行评估

企业应用发布流程思路

列式存储、行式存储区别

  • 列式存储:Hbase、cassandra
  • 行式存储:mysql、sqlserver

读取数据

新增、修改、删除数据

  • 新增:多个列族,并发写磁盘
  • 更新:添加一个新的数据(不修改原先数据),并标明版本号,读取时会读取最新版本号的数据,定时删除旧版本号数据
  • 删除:添加删除标记(keyType=delete),类似于数据库假删除

千万级订单系统高可用设计

避免丢单要点

  • 关键逻辑不要读写分离、缓存查询方式,避免从库同步延时导致订单无法被查询到,从而创建支付单失败(主库已写入,还未同步到从库,缓存同理)

  • 订单补偿不要粗暴地使用消息队列的方式,避免中间件发送消息和接收消息时引发的订单丢失

  • 接收消息处理失败时一定要让消息重试,避免丢失

避免锁表

在数据库事务里同时去更新其他数据源,或发送 MQ 消息等,这不仅不能保证数据一致性,还会把数据库的连接耗尽

千万级订单系统设计方案

  • 将下单服务进行了服务拆分,使用单独的接单服务处理接单
  • 使用订单引擎和订单管道处理订单业务逻辑
  • 改用双写和数据补偿的方式处理缓存
  • 使用缓存过期的方式控制数据量

Zookeeper分布式锁实现原理

Zookeeper的数据结构是树形的,节点叫做Znode,Znode共分为有4种类型

  • 持久节点
  • 持久节点顺序节点
  • 临时节点
  • 临时顺序节点

持久节点断开连接依旧存在,临时节点则会被删除,所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号

Zookeeper分布式锁实现代码

  • pom中引入curator-recipes,来简化Zookeeper的实现
1
2
3
4
5
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator‐recipes</artifactId>
<version>5.2.0</version>
</dependency>
  • 代码流程
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
public int outOfWarehouseWithLock() throws Exception {
//设置ZK客户端重试策略, 每隔5秒重试一次,最多重试10次
RetryPolicy policy = new ExponentialBackoffRetry(5000, 10);

//创建ZK客户端,连接到Zookeeper服务器
CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.31.103:2181").retryPolicy(policy).build();
//创建与Zookeeper服务器的连接
client.start();

//声明锁对象,本质就是ZK顺序临时节点
final InterProcessMutex mutex = new InterProcessMutex(client, "/locks/wh-shoe");
try {
//请求锁,创建锁
mutex.acquire();
//处理业务逻辑
if (WarehouseService.shoe > 0) {
Thread.sleep(1000);
//扣减库存
return --WarehouseService.shoe;
} else {
//库存不足,
throw new RuntimeException("运动鞋库存不足");
}
} finally {
//释放锁
mutex.release();
}
}

大型电商秒杀架构

  • 利用了redis中decr命令的原子性来保证库存不会超卖

  • lua脚本的作用是:监听请求,发送decr 商品编号命令到redis中,实现商品库存自减,自减后判断库存是否小于0,如果是则直接返回“已无库存”,如果不是,则将订单信息发送给MQ(秒杀瞬时流量过大,需要MQ进行限流),返回订单编号即可(接下来的操作与lua脚本无关)

  • 通过MQ的限流让订单程序可以正常的运行,最后订单的状态由APP轮询获得

解决高并发热点数据的访问倾斜问题

热点数据特征

短时访问频率超高,数据总量相对较少

方案一 :专门设置热点数据缓存(高成本)

关于热点的选择,一种是将所有大促的商品放入热点缓存内,适合小规模活动,另一种是根据以往的用户行为分析大数据评估,将个别数据选为热点数据

方案二:缓存前置+闪电缓存(低成本)

缺点是会造成短时间的缓存数据不一致,但是节约了成本

日志收集架构

  • ELK:es、logstash、kb三款软件进行监管
  • TCP推送:在ELK的基础上,应用通过Logback插件LogstashTcpSocketAppender插件进行推送
  • EFK:es、filebeat(监视指定的位置或者日志文件将其转发到logstash或者es,只能选一个,无法同时向多个output发送) 、kb三款软件进行监管
  • kefk:es、filebeat、kb、kafka(用来解决filebeat无法同时向多个output发送的问题)
赏个🍗吧
0%