利用主流 MQ 中的延迟消息功能,消息发送到 Broker 上以后并不会立刻投递,而是根据消息中设置的延迟时间去投递。我们只需要把新的订单并计算这个订单的超时时间发送到 MQ 中即可。MQ 这种实现方式在性能、可扩展性、稳定性上都比较好,是一个不错的选择。
- 定时轮询, 有延迟,数据库压力大
- 惰性取消,查询时取消,影响查询效率
- JDK延迟队列DelayQueue,
- 时间轮:Netty的HashedWheelTimer;异常恢复困难,集群扩展麻烦,内存占用。
- Redis过期回调:需要额外维护redis。
- Redis有序集合:可能重复消费同一key。
- 任务调度:使用任务调度中间件xxl-job、ScheduleX、Elastic-Job等来实现,设置一个调度时间cron,到达订单过期的调度时间时,触发任务执行取消订单业务逻辑。实现复杂,维护成本高。
- 消息队列:使用RocketMQ、RabbitMQ、Kafka的延时消息,消息在发送到消息队列服务端后并不会立马投递,而是根据消息中的属性延迟固定时间后才投递给消费者。
- 幂等:一个接口,使用相同的参数重复执行的情况下,对数据造成的改变只发生一次。
- 保证幂等就是:识别当前请求是重复请求:
要么就是接口只允许调用一次,比如唯一约束、基于 redis 的锁机制。
要么就是对数据的影响只会触发一次,比如幂等性、乐观锁
- 2.1 使用数据库唯一索引的方式实现
- 2.2 使用 Redis 里面的 setNx 命令
- 2.3 使用状态机的方式来实现幂等
“站内信”有两个基本功能:
- 点到点的消息传送。用户给用户发送站内信,管理员给用户发送站内信。
- 点到面的消息传送。管理员给用户(指定满足某一条件的用户群)群发消息
需要设计一个消息内容表和一个用户通知表,
当创建一条系统通知后,数据插入到消息内容表。消息内容包含了发送渠道,根据发送渠道决定后续动作。
如果是站内渠道,在插入消息内容后异步的插入记录到用户通知表。
消息内容表:id,标题,内容,类型,发送人,发送渠道(站内、短信、推送)
用户通知表:id,消息id,已读状态,接受者,类型
- 会带来两个问题:
-
- 随着用户量的增加,发送一次消息需要插入到数据库中的数据量会越来越大,导致耗时会越来越长
-
- 用户通知表的数据量会非常大,对未读消息的查询效率会严重下降
- 解决方案:
- 方案一:
- 先取消用户通知表, 避免在发送平台消息的时候插入大量重复数据问题。
- 其次增加一个“message_offset” 站内消息进度表,每个用户维护一个消息消费的进度 Offset。每个用户去获取未读消息的时候,只需要查询大于当前维护的 msg_id_offset 的数据即可。
- 在这种设计方式中,即便我们发送给 10W 人,也只需要在消息内容表里面插入一条记录即可。在性能上和数据量上都有较大的提升。
- 方案二:
- 使用 Redis 中的 Set 集合来保存已经读取过的消息 id。使用 userid_read_message 作为 key,这样就可以为每个用户保存已经读取过的所有消息的 id。
- 当用户读取了未读消息后, 就直接在 redis 的已读消息 id 的 set 中新增一条记录。
- 这样,在已经得知到已读消息的数量和具体消息 id 的情况下,我们可以直接使用消息id 来查询没有消费过的数据。
- 想判断一个元素是否存在某个集合里
- BitMap 的基本原理就是用一个 bit 位来存储当前数据是否存在的状态值,也就是把一个数据通过 hash 运算取模后落在 bit 位组成的数组中,通过 1 对该位置进行标记。(适用于大规模数据,但数据状态又不是很多的情况)
- 布隆过滤器就是在位图的基础上做的一个优化设计:它的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点,把它们置为 1。
- 检索的时候,使用同样的方式去映射,只要看到每个映射的位置的值是不是 1,就可以大概知道该元素是否存在集合中了。如果这些点有任何一个 0,则被检查的元素一定不在;如果都是 1,则被检查的元素很可能存在。
- 限流算法是一种系统保护策略,主要是避免在流量高峰导致系统被压垮,造成系统不可用的问题。
- 常见的限流算法:
- 2.1 计数器限流: 一般用在单一维度的访问频率限制上,比如短信验证码每隔60s 只能发送一次;或者接口调用次数等.它的实现方法很简单,每调用一次就加 1,处理结束以后减一
- 2.2 滑动窗口限流:
,本质上也是一种计数器,只是通过以时间为维度的可滑动窗口设计,来减少了临界值带来的并发超过阈值的问题。每次进行数据统计的时候,只需要统计这个窗口内每个时间刻度的访问量就可以了。Spring Cloud 里面的熔断框架 Hystrix ,以及 Spring Cloud Alibaba 里面的 Sentinel都采用了滑动窗口来做数据统计。
- 2.3 漏桶算法: 一种恒定速率的限流算法,不管请求量是多少,服务端的处理效率是恒定的。基于 MQ 来实现的生产者消费者模型,其实算是一种漏桶限流算法。
- 2.4 令牌桶算法:
- 相对漏桶算法来说,它可以处理突发流量的问题。
- 它的核心思想是,令牌桶以恒定速率去生成令牌保存到令牌桶里面,桶的大小是固定的,令牌桶满了以后就不再生成令牌。
- 每个客户端请求进来的时候,必须要从令牌桶获得一个令牌才能访问,否则排队等待。
- 在流量低峰的时候,令牌桶会出现堆积,因此当出现瞬时高峰的时候,有足够多的令牌可以获取,因此令牌桶能够允许瞬时流量的处理。
- 网关层面的限流、或者接口调用的限流,都可以使用令牌桶算法,像 Google 的 Guava,和 Redisson 的限流,都用到了令牌桶算法
- 限流的本质是实现系统保护,最终选择什么样的算法,一方面取决于统计的精准度,另一方面考虑限流维度和场景的需求。
- 一致性 hash,是一种比较特殊的 hash 算法,它的核心思想是解决在分布式环境下,hash 表中可能存在的动态扩容和缩容的问题
- 原理:
- 2.1 一致性 Hash 是通过一个 Hash 环的数据结构来实现的,这个环的起点是 0,终点是 2^32-1。
- 2.2 然后我们把存储节点的 ip 地址作为 key 进行 hash 之后,会在 Hash 环上确定一个位置。
- 2.3 接下来,(如图)就是把需要存储的目标 key 使用 hash 算法计算后得到一个 hash 值,同样也会落到 hash 环的某个位置上。
- 2.4 然后这个目标 key 会按照顺时针的方向找到离自己最近的一个节点进行数据存储。
- 2.5 如果新增或删除节点只会影响相邻的节点
- ,一致性 hash 算法的好处是扩展性很强,在增加或者减少服务器的时候,数据迁移范围比较小。另外,在一致性 Hash 算范里面,为了避免 hash 倾斜导致数据分配不均匀的情况,我们可以使用虚拟节点的方式来解决。
- 生产环境服务器处理效率变慢,主要会涉及到三个纬度:CPU 的利用率、磁盘 IO 效率、内存
- CPU 利用率过高或者 CPU 利用率过低,都会影响程序的处理效率。
- 2.1 利用率过高,说明当前服务器要处理的指令比较多,当 CPU 忙不过来的时候,指令的运算效率自然就会下降;可以使用 top 命令查询当前系统中占用 CPU 过高的进程,以及定位到这个进程中比较活跃的线程。再通过 jstack 命令打印当前虚拟机的线程快照,然后根据快照日志排查问题代码。
- 2.2 CPU 利用率过低,说明程序资源使用不够,可以增加线程数量提升程序性能。
- 磁盘的 IO 效率:使用 iostat 命令查看,如果磁盘负载较高,可以针对性的进行优化,比如
- 3.1 借助缓存系统,减少磁盘 IO 次数
- 3.2 用顺序写替代随机写入,减少寻址开销
- 3.3 使用 mmap 替代 read/write,减少内存拷贝次数
- 3.4 另外,系统 IO 的瓶颈可以通过 CPU 和负载的非线性关系体现出来。当负载增大时,系统吞吐量不能有效增大,CPU 不能线性增长,其中一种可能是 IO 出现阻塞。
- 内存的瓶颈:
- 4.1 内存使用率比较高的时候, 可以 dump 出 JVM 堆内存,然后借助 MAT 工具进行分析,查出大对象或者占用最多的对象,以及排查是否存在内存泄漏的问题。
- 4.2 如果 dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题,需要借助操作系统指令 pmap 查出进程的内存分配情况。
- 4.3 如果 CPU 和 内存使用率都很正常,那就需要进一步开启 GC 日志,分析用户线程暂停的时间、各部分内存区域 GC 次数和时间等指标,可以借助 jstat 或可视化工具 GCeasy 等,
- 4.4 如果问题出在 GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调优、使用更适合的垃圾收集器;分析 jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启 jmx,使用 visualmv 等可视化工具远程监控与分析。
- 单表数据量:如果单个表的数据量已经非常大,例如超过了百万级别,就需要开始考虑分表。
- 数据库性能:当单个数据库的性能无法满足业务需求时,就需要考虑分库。
- 数据访问频率:如果某些表的数据访问频率非常高,单个数据库节点无法满足高并发请求,就需要考虑将这些表分到不同的库或表中,以提高性能
- 业务拆分:当系统的业务逻辑越来越复杂,不同的业务之间的数据耦合度越来越低,就需要考虑对系统进行拆分,以方便管理和扩展。
- 磁盘 IO:数据量大意味着需要从磁盘中读取更多的数据,而磁盘 IO 速度是相对较慢的,因此会影响查询效率。
- 索引失效:索引是提高查询效率的重要手段,但是如果索引失效,就会导致查询效率下降。
- 索引失效的原因可能是查询条件中使用了不支持索引的操作符,或者是数据分布不均匀导致索引失效。
- 数据分页:当需要查询大量数据的时候,数据库需要进行数据分页,而数据分页的过程需要占用大量的 CPU 资源,因此也会影响查询效率。 4. 锁竞争:当多个事物同时对同一个表进行读写操作时,就会产生锁竞争,而锁竞争会导致查询效率下降。
- 内存使用:当表数据量大的时候,需要占用更多的内存空间来缓存数据,而如果内存不足,就会导致数据库频繁地进行磁盘 IO,从而影响查询效率。
- 从接口本身的实现维度来说,可以从几个方面来优化:
- 如果在接口中有操作数据库层面的代码,可以优化数据库 IO 的效率,比如 SQL优化、数据库层面的优化等
- 如果存在部分频繁访问数据库的热数据,可以采用缓存机制
- 如果涉及到远程调用或者耗时的方法调用,可以采用异步方式避免同步阻塞,从而提升程序运行效率
- 代码本身的优化,可以利用合适的算法减少时间复杂度、避免一些很明显的重复计算等问题
- 从宏观链路维度来说,可以关注几个方面:
- 网络带宽,带宽的大小会影响数据的传输效率
- 服务器硬件资源如 CPU、内存等,会影响到接口中代码的执行效率
- 单个部署节点的计算能力瓶颈,也会影响接口性能,可以采用分布式部署的方式来优化
- 总的来说,接口的性能优化涉及到的因素比较多,如果真的出现性能问题,可以根据系统日志以及压测的情况去分析瓶颈点再针对性的优化
- 安全性问题,由于和第三方接口对接涉及到数据的跨网络传输,为了防止数据被拦截和篡改,需要采用安全的通信机制,比如 https 协议,以及数据签名等。
- 接口稳定性和可靠性,这两个方面会直接影响用户体验和业务的正常流转,所以需要做相对充分的评估和测试
- 接口是否存在访问限制或者费用,如果存在并发量的限制,需要评估是否满足当前业务需求; 如果存在费用,需要评估是否符合预算
- 雪花算法一般用来实现全局唯一的业务主键,解决分库分表之后主键 id 的唯一性问题。
- 全局唯一id,可以有多个实现方式:uuid,redis原子递增,数据库全局表自增id等
- 但除了全局唯一性外,一般还需满足有序递增、高性能、带时间戳等。
- snowflake它是由一个 64 位的 long 类型数字组成,分为四个部分:
- 4.1 第一个 bit 位是符号位,因为 id 不会是负数,所以它一般是 0
- 4.2 41 个 bit 位来表示毫秒单位的时间戳
- 4.3 10 个 bit 位来表示工作机器 id
- 4.4 12 个 bit 位表示递增的序列号(每毫秒2^12 = 4096个)
- 特性:全局唯一性;趋势递增(依赖时间戳);信息安全;
2035年将取消闰秒;
雪花算法支持约2^41/1年=69.7年
最发并发:2^12=4096/ms
时钟回拨:
1. 异常处理,排除异常,停止生成
2. 等待时间恢复
3. 备用id
4. 多时钟法