使用 Go 优化我们的接口

代码星冰乐

专注成就未来

首页 归档 关于

使用 Go 优化我们的接口

Dec 23, 2019 | haifeiWu | Go | 阅读
文章目录
  1. 1. 背景
  2. 2. 缓存优化方案
    1. 2.1. 方案一
    2. 2.2. 方案二
    3. 2.3. 方案三(最终采用的方案)
  3. 3. 并发优化方案
    1. 3.1. 使用 Goroutine 来优化我们的串行逻辑
    2. 3.2. golang 中的 map 是线程不安全的
  4. 4. 使用策略模式优化我们的逻辑
  5. 5. 小结

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2019/41d1/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。


由于版权原因,请阅读原文 --> 使用 Go 优化我们的接口

关注我们

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2019/41d1/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2019/41d1/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

标题起的是有点大,不过还好本片文章主要也是使用 Go 来优化 HTTP 服务的,也算打个擦边球吧~

背景

特征数据暴增,导致获取一个城市下所有的特征的接口延时高,下面是监控上看到的接口响应耗时,最慢的时候接口响应时间能达到 5s 多。
cost-time

缓存优化方案

代码优化思路:

1,使用缓存

1.1为什么使用内存,而不是 Redis?

分析业务需求,当前需要存储起来的数据是ObjectId,ObjectId 是一个长度为14左右的字符串,我们假设平均下来ObjectId是长度为16的字符串,这样算下来就是每个ObjectId占用的内存大小是2个字节,当前业务需要存储的ObjectId大概是30万条,这样算下来当前业务需要存储的ObjectId要占用的内存在0.5M完全可以在内存中进行操作。相比于使用 Redis 来说没有网络开销,效率更高。

1.2,缓存初始化:当服务启动时,本地缓存初始化为空。

1.3 关于缓存版本的概念

缓存版本是离线特征生产任务更新后将数据版本更新到 fusion 中。

下面三种方案都是基于内存存储 ObjectId 数据,在内存更新的时候策略有所不同。

方案一

2.1 缓存更新

使用主动更新缓存的方式,创建定时任务,每间隔1分钟查一次fusion的数据版本,若更新则更新缓存中的数据。

2.2 缺点

单独启动一个缓存更新线程,代码不好维护,也会有定时任务线程挂掉的情况,不易发现。还有就是需要提前把相关参数配置到代码中或者引入配置中心,维护成本较高。

方案二

3.1 缓存更新

采用被动触发的缓存更新策略,由接口调用触发。请求进来后检测当前缓存中的数据的版本与fusion中的数据版本是否一致,若版本更新,则重新读取当前请求对应城市的所有feature数据到缓存中,并将更新后的数据返回给调用方。

3.2 缺点

由于是被动触发的是同步更新缓存的,容易造成接口调用时如果正好遇上版本更新,需要更新数据到内存中,会出现偶现的毛刺。

3.3 业务执行时序图
方案二时序图

方案三(最终采用的方案)

4.1,缓存更新

采用被动更新缓存的策略,由接口调用方触发。若当前缓存中有数据则直接返回缓存中的数据,然后检测当前缓存中的数据的版本与fusion中的数据版本是否一致,若版本更新,则重新读取当前请求对应城市的所有feature数据到缓存中,反之结束缓存更新逻辑。

4.2 业务执行时序图
方案三时序图

并发优化方案

使用 Goroutine 来优化我们的串行逻辑

Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。

为了更好理解Goroutine,现讲一下线程和协程的概念

线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。

协程(coroutine):又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。

和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。

golang 中的 map 是线程不安全的

很显然,我们可以用锁机制解决 Map 的并发读写问题。我们将map结构改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// M
type M struct {
Map map[string]string
lock sync.RWMutex // 加锁
}

// Set ...
func (m *M) Set(key, value string) {
m.lock.Lock()
defer m.lock.Unlock()
m.Map[key] = value
}

// Get ...
func (m *M) Get(key string) string {
m.lock.RLock()
defer m.lock.RUnlock()
return m.Map[key]
}

在上面的代码中,我们引入了锁机制操作,从而保证了map在多个goroutine中的安全。

使用策略模式优化我们的逻辑

这块主要是因为代码中存在太多的 if/else ,故采用策略模式来优化我们的代码结构。这里先放上一篇网上找到的文章,之后有时间再单独出一篇相关文章吧。优化后的代码相较于之前代码量少了 50% ,更加清晰与便于维护。下面是优化的代码上线后的效果,请求耗时都在100ms以下:
监控接口耗时

小结

上面整体介绍了下当我们的接口耗时较长的时候的一般处理方案,当然具体问题还得具体分析,所以当出现接口反应慢的情况的时候,我们应该具体分析接口反应慢的具体原因,方可对症下药!

关注我们

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2019/41d1/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

分享
使用 Map 实现策略模式如何使用 Redis 实现分布式锁
微信关注我们
分类
  • Android8
  • Go4
  • Java59
  • Kafka,Java1
  • Kotlin2
  • Linux1
  • MapReduce1
  • Python2
  • Raft1
  • Redis1
  • ThreadPoolExecutor1
  • go1
  • 工具1
  • 总结8
  • 旅游日记1
标签
Nginx ChanghuiN haifeiWu Android Java 设计模式 hexo Kotlin 算法 MySQL 源码解析 Python Redis golang web Kafka 配置中心 总结 性能优化 旅游日记 Shell Go 问题排查 译文 Docker Spring Boot 工具 学习笔记 WebFlux 性能测试 go 散列表 源码 netty Raft
最近文章
  • Kafka的日志复制机制
  • 从20到21
  • go 并发编程
  • 【译】了解Linux CPU负载-您何时应该担心?
  • Zookeeper 与分布式锁
  • 基于Redis的分布式锁到底安全吗?
  • 【译】Raft 学生指南
  • ThreadPoolExecutor 的简单梳理
  • MapReduce 的简单实现
  • 使用 Map 实现策略模式
福利专区
    免费SSL证书
      阿里云红包
        腾讯云专属福利
        Copyright © 2021 代码星冰乐. Powered by ChanghuiN. 版权所有 晋ICP备15001365号
        特别感谢: 云服务器服务商 、 CDN 服务商