首页
网站导航
关于
Search
1
解决Typecho Joe主题访问fastly.jsdelivr.net速度慢的方案 | 快速优化技巧
2,191 阅读
2
解决WSL2内存不释放问题的最佳指南
1,407 阅读
3
如何在 Typecho Joe 主题的文章中增加目录
1,171 阅读
4
GO语言环境的搭建教程 - 完全指南
1,005 阅读
5
如何解决Win11电脑桌面上方显示横线问题 | 窗口11教程
1,005 阅读
默认分类
编程语言
GO语言
PHP
Node
javascript
html
rust
java
Css
Python
资源分享
chrome插件
阅读思考
运维架构
redis
Nginx
linux
memcached
mongodb
mysql
windows
docker
k8s
Mq
apache
CI
Git
swoole
elk
系统设计
thinkPhp
beego
登录
Search
标签搜索
重要
go基础
git 命令
go包
phpstorm
sublime
thinkphp6
mysql问题
软件分享
redis命令
php基础
thinkphp3.2
php第三扩展包
小蚯蚓博客
累计撰写
333
篇文章
累计收到
48
条评论
首页
栏目
默认分类
编程语言
GO语言
PHP
Node
javascript
html
rust
java
Css
Python
资源分享
chrome插件
阅读思考
运维架构
redis
Nginx
linux
memcached
mongodb
mysql
windows
docker
k8s
Mq
apache
CI
Git
swoole
elk
系统设计
thinkPhp
beego
页面
网站导航
关于
搜索到
67
篇与
的结果
2024-04-08
解决PHPStorm参数提示和使用次数提示在右边
暂无简介
2024年04月08日
33 阅读
0 评论
0 点赞
2024-02-25
Golang性能分析 | 使用pprof进行性能优化
什么是 pprof pprof 是 Go 官方提供的性能分析工具,可以分析程序的运行情况,并且提供可视化的功能。prof 是 profile(画像)的缩写,使用 pprof 可以分析以下几种指标数据: allocs:程序启动之后内存分配的情况 block:导致阻塞操作的一些堆栈跟踪信息 cmdline:当前程序启动的命令行 goroutine:所有当前 goroutine 的堆栈跟踪信息 heap:程序在当前堆上内存分配的情况 mutex:锁资源的竞争的堆栈信息 profile:CPU profile 文件。可以在 debug/pprof?seconds=x秒 GET 参数中指定持续时间。获取 pprof 文件后,使用 go tool pprof x.prof 命令分析 pprof 文件。 threadcreate:系统线程的使用情况 trace:当前系统的代码执行的链路情况 使用 pprof 工具主要分析以下几种指标: CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置 Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏 Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置 Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况。 当程序存在内存或者 CPU 飙升的情况时,我们可以通过 pprof 工具来查询问题出现的根源。 怎么使用 pprof pprof 包含两个相关的库: runtime/pprof 主要应用于工具型应用。包含脚本、定时任务等。 如:对于只跑一次的程序,例如每天只跑一次的离线预处理程序,调用 pprof 包提供的函数,手动开启性能数据采集 net/http/pprof 主要应用于服务型应用。包含 HTTP 服务,GRPC 服务等。 如:对于在线服务,对于一个 HTTP Server,访问 pprof 提供的 HTTP 接口,获得性能数据。当然,实际上这里底层也是调用的 runtime/pprof 提供的函数,封装成接口对外提供网络访问。 工具型应用 工具型应用主要使用 runtime/pprof 包实现性能分析。 func main() { // --- cpu 分析示例 start--- // 创建cpu分析文件 fc, err := os.Create("./cpu.prof") if err != nil { fmt.Println("create cpu.prof err:", err.Error()) return } defer fc.Close() // 开始分析cpu err = pprof.StartCPUProfile(fc) if err == nil { defer pprof.StopCPUProfile() } // --- cpu 分析示例 end--- var count int for i := 0; i < 10000; i++ { count++ } // --- 内存 分析示例 start--- fm, err := os.Create("./memory.prof") if err != nil { fmt.Println("create memory.prof err:", err.Error()) return } defer fm.Close() // 开始分析内存 err = pprof.WriteHeapProfile(fm) if err != nil { fmt.Println("write heap prof err:", err.Error()) return } // --- 内存 分析示例 end--- for i := 0; i < 10000; i++ { count++ } fmt.Println("do finish......count:", count) } 执行 go run main.go 后,在代码目录下,可以看到生成了 cpu.prof 和 memory.prof 文件。 通过执行 go tool pprof ./memory.prof 或者 go tool pprof -http=:8888 ./memory.prof 进入命令行模式或者 web 页面进行性能分析。 执行 go tool pprof ./memory.prof 进入命令行: 执行 go tool -http=:8888 pprof ./memory.prof 可进入 web 页面,更方便查看: 页面展示效果: SAMPLE 各个标签的含义解释: 服务型应用 对于服务类型的应用,主要在服务内部匿名引入 net/http/pprof 包,然后通过 HTTP 访问 pprof 页面。 匿名引入方式为:import _ "net/http/pprof" package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { http.HandleFunc("/", hello) err := http.ListenAndServe(":8080", nil) if err != nil { fmt.Println("ListenAndServe Err:", err.Error()) return } } func hello(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "Hello World, Are You OK?") } 执行 http://localhost:8080/debug/pprof/ 可以看到画像信息: 但是需要注意,如果 HTTP 服务不是通过 http.ListenAndServe(":8080", nil) 启动的,而是指定第二个参数启动的话,需要自己注册 pprof 路由。 在 net/http/pprof/pprof.go 的官方源码注释中也提到此种情况: If you are not using DefaultServeMux, you will have to register handlers with the mux you are using. 如果您不使用 DefaultServeMux,则必须向所使用的多路复用器注册 pprof 处理程序 http. ListenAndServe 函数可以传递 handler,如果 handler 不为 nil,则说明研发自定义了 ServeMux,否则用的是默认 DefaultServeMux net/http/pprof 包中,有 init 函数 func init() { http.HandleFunc("/debug/pprof/", Index) http.HandleFunc("/debug/pprof/cmdline", Cmdline) http.HandleFunc("/debug/pprof/profile", Profile) http.HandleFunc("/debug/pprof/symbol", Symbol) http.HandleFunc("/debug/pprof/trace", Trace) } 所以如果使用默认 ServeMux,则不需要注册,但是如果使用自定义的 ServeMux,则需要增加注册后,才能获取到 pprof。 // 自己注册这几个函数 r.HandleFunc("/debug/pprof/", pprof.Index) r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) r.HandleFunc("/debug/pprof/profile", pprof.Profile) r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) r.HandleFunc("/debug/pprof/trace", pprof.Trace) 另外一种启动 pprof 的方式在生产环境中更加常用:通过协程另起一个 HTTP 服务,单独用作 pprof 分析: func RegisterProf() { go func() { if err := http.ListenAndServe(":6060", nil); err != nil { panic("pprof server start error: " + err.Error()) } }() } pprof 数据分析 GO 官方提供了 go tool pprof 工具来帮助我们分析 pprof 生成的数据文件。 使用 go tool pprof 分析数据,主要有两种模式: 命令行交互模式 go tool pprof [file_pprof|url_pprof] web 页面模式 go tool pprof -http=:6666 [file_pprof|url_pprof] 其中,file_pprof 表示生成的 prof 分析文件,如 cpu.prof;url_pprof 表示远端服务开启的 pprof 访问,如 http://localhost:8080/debug/pprof/profile 进入命令行交互模式后,可以使用 help 查看所有子命令,使用 help <cmd|option> 查看子命令使用方法。如 help、help top 等 CPU Profiling 浏览器访问 /debug/pprof/profile 会自动进行 CPU profiling,默认持续 30 s,并生成一个文件供下载,可以通过带参数 ?seconds=60 进行 60 秒的数据采集。 为了模拟请求,使用 ab 进行压测:ab -k -c 1 -t 180 -n 100000000 http://localhost:8080/hello 执行 go tool pprof http://localhost:8080/debug/pprof/profile 后,默认需要等 30 s 才会显示交互 top 指令排序展示 每一行表示一个函数的信息,列信息字段解释: flat:函数在 CPU 上运行的时间 flat%:函数在 CPU 上运行时间的百分比 sum%:是从第一行到当前行所有函数累加使用 CPU 的比例,如第二行 sum=53.85=30.77+23.08 cum:这个函数以及子函数运行所占用的时间,应该大于等于 flat cum%:这个函数以及子函数运行所占用的比例,应该大于等于 flat% 最后一列:函数的名字 web 指令生成图示 在交互模式下输入 web,会自动生成一个 svg 文件,并跳转到浏览器打开。 改功能需要安装 graphviz 后才能使用,安装方法 https://shidawuhen.github.io/2020/02/08/go-callvis/。 图中每个方框对应应用程序运行的一个函数,方框越大代表函数执行的时间越久(函数执行时间会包含它调用的子函数的执行时间,但并不是正比的关系);方框之间的箭头代表着调用关系,箭头上的数字代表被调用函数的执行时间。 具体细节可以参考:https://github.com/google/pprof/tree/master/doc#interpreting-the-callgraph 以 runtime netpoll 为例: 箭头上的 230 ms 表示该函数总共的执行时间,包含自身和下游所有子节点; 方框中的 10 ms 表示自身的执行时间; 方框中的 2.70% 表示自身执行时间在该函数总共执行时间的占比在总时间中的占比:2.70 = (10/230)*62.16 方框中的 62.16% 表示总时间占比,即整个程序耗时占比。 list 指令分析函数 确定出哪个函数耗时之后,可以用 pprof 分析函数中的哪一行导致的耗时,使用子命令:list 函数名。 堆内存分析示例 内存分配既可以发生在堆上也可以在栈上。堆上分配的内存需要垃圾回收或者手动回收(对于没有垃圾回收的语言,例如 C++),栈上的内存则通常在函数退出后自动释放。 Go 语言通过逃逸分析会将尽可能多的对象分配到栈上,以使程序可以运行地更快。 这里说明一下,有两种内存分析策略: 一种是当前的(这一次采集)内存或对象的分配,称为 inuse; 另一种是从程序运行到现在所有的内存分配,不管是否已经被 gc 过了,称为 alloc。 As mentioned above, there are two main memory analysis strategies with pprof. One is around looking at the current allocations (bytes or object count), called inuse. The other is looking at all the allocated bytes or object count throughout the run-time of the program, called alloc. This means regardless if it was gc-ed, a summation of everything sampled. 加上 -sample_index 参数后,可以切换内存分析的类型: go tool pprof -sample_index=alloc_space http://localhost:8080/debug/pprof/heap 或者 go tool pprof -alloc_space http://localhost:8080/debug/pprof/heap 四种标签: pprof 数据分析类型汇总 其他数据的分析和 CPU 的基本一致。下面列举所有的分类: http://localhost:8080/debug/pprof/ :获取概况信息,即本文第一张图的信息 go tool pprof http://localhost:8080/debug/pprof/allocs : 分析内存分配 go tool pprof http://localhost:8080/debug/pprof/block : 分析堆栈跟踪导致阻塞的同步原语 go tool pprof http://localhost:8080/debug/pprof/cmdline : 分析命令行调用的程序,web 下调用报错 go tool pprof http://localhost:8080/debug/pprof/goroutine : 分析当前 goroutine 的堆栈信息 go tool pprof http://localhost:8080/debug/pprof/heap : 分析当前活动对象内存分配 go tool pprof http://localhost:8080/debug/pprof/mutex : 分析堆栈跟踪竞争状态互斥锁的持有者 go tool pprof http://localhost:8080/debug/pprof/profile : 分析一定持续时间内 CPU 的使用情况 go tool pprof http://localhost:8080/debug/pprof/threadcreate : 分析堆栈跟踪系统新线程的创建 go tool pprof http://localhost:8080/debug/pprof/trace : 分析追踪当前程序的执行状况 参考连接 一文搞懂pprof 深度解密Go语言之 pprof go性能分析工具pprof Go语言:利用pprof工具排查内存泄漏的示例 Go语言:利用pprof工具查找goroutine(协程)泄漏的示例 go 程序性能调优 pprof 的使用
2024年02月25日
60 阅读
0 评论
0 点赞
2024-02-19
ODS到ADS详解:数仓分层策略与实践
2024-02-19 为什么要对数据仓库分层 只有数据模型将数据有序的组织和存储起来之后,大数据才能得到高性能、低成本、高效率、高质量的使用。 分层意义 清晰数据结构:每一个数据分层都有它的作用域,这样我们在使用表的时候能更方便地定位和理解。 数据溯源追踪:我们最终给业务提供的是一能直接使用的张业务表,但是它的来源有很多,如果有一张来源表出问题了,我们希望能够快速准确地定位到问题,并清楚它的危害范围。 数据复用,减少重复开发:规范数据分层,下层可以使用上层的一些数据。 把复杂问题简单化:将一个复杂的任务分解成多个步骤来完成,每一层只处理单一的步骤,比较简单和容易理解。 数据仓库(ETL)的四个操作 ETL (extraction transformation loading)负责将数据源中的数据抽取到临时中间层后进行清洗、转换、集成,最后加载到数据仓库或数据集市中。 ETL 是实施数据仓库的核心和灵魂,ETL 规则的设计和实施约占整个数据仓库搭建工作量的 60%~80%。 数据抽取 (extraction):包括初始化数据装载和数据刷新:初始化数据装载主要关注的是如何建立表,并把相应的数据放到这些数据表中;而数据刷新关注的是当源数据发生变化时如何对数据仓库中的相应数据进行追加和更新等维护 (比如可以创建定时任务,或者触发器的形式进行数据的定时刷新)。 数据清洗:主要是针对源数据库中出现的二义性、重复、不完整、违反业务或逻辑规则等问题的数据进行统一的处理。即清洗掉不符合业务或者没用的的数据。比如通过编写 hive 或者 MR 清洗字段中长度不符合要求的数据。 数据转换 (transformation):主要是为了将数据清洗后的数据转换成数据仓库所需要的数据:来源于不同源系统的同一数据字段的数据字典或者数据格式可能不一样 (比如 A 表中叫 id, B 表中叫 ids),在数据仓库中需要给它们提供统一的数据字典和格式,对数据内容进行归一化;另一方面,数据仓库所需要的某些字段的内容可能是源系统所不具备的,而是需要根据源系统中多个字段的内容共同计算确定。 数据加载(loading):是将最后上面处理完的数据导入到对应的存储空间里(hbase,mysql 等)以方便给数据集市提供,进而可视化。 一般大公司为了数据安全和操作方便,都是自己封装的数据平台和任务调度平台,底层封装了大数据集群比如 hadoop 集群,spark 集群,sqoop, hive, zookeepr, hbase 等只提供 web 界面,并且对于不同员工加以不同权限,然后对集群进行不同的操作和调用。 以数据仓库为例,将数据仓库分为逻辑上的几个层次。这样对于不同层次的数据操作,创建不同层次的任务,可以放到不同层次的任务流中进行执行(大公司一个集群通常每天的定时任务有几千个等待执行,甚至上万个,所以划分不同层次的任务流,不同层次的任务放到对应的任务流中进行执行,会更加方便管理和维护)。 分层的误区 数仓层内部的划分不是为了分层而分层,分层是为了解决 ETL 任务及工作流的组织、数据的流向、读写权限的控制、不同需求的满足等各类问题。 业界较为通行的做法将整个数仓层又划分成了 DWD、DWT、DWS、DIM、DM等很多层。然而我们却始终说不清楚这几层之间清晰的界限是什么,或者说我们能说清楚它们之间的界限,复杂的业务场景却令我们无法真正落地执行。 所以数据分层这块一般来说三层是最基础的,至于DW层如何进行切分,是根据具体的业务需求和公司场景自己去定义。 数据仓库的技术架构 技术架构,这部分了解即可 数据中台包含的内容很多,对应到具体工作中的话,它可以包含下面的这些内容: 系统架构:以Hadoop、Spark等组件为中心的架构体系 数据架构:顶层设计,主题域划分,分层设计,ODS-DW-ADS 数据建模:维度建模,业务过程-确定粒度-维度-事实表 数据管理:资产管理,元数据管理、质量管理、主数据管理、数据标准、数据安全管理 辅助系统:调度系统、ETL系统、监控系统 数据服务:数据门户、机器学习数据挖掘、数据查询、分析、报表系统、可视化系统、数据交换分享下载 数仓分层架构 数据仓库标准上可以分为四层。但是注意这种划分和命名不是唯一的,一般数仓都是四层,但是不同公司可能叫法不同。但是核心的理念都是从四层数据模型而来。 贴源层(ODS, Operational Data Store) 数据引入层(ODS,Operational Data Store,又称数据基础层):将原始数据几乎无处理地存放在数据仓库系统中,结构上与源系统基本保持一致,是数据仓库的数据准备区。这一层的主要职责是将基础数据同步、存储。 一般来说 ODS 层的数据和源系统的数据是同构的,主要目的是简化后续数据加工处理的工作。从数据粒度上来说 ODS 层的数据粒度是细的。ODS 层的表通常包括两类: 用于存储当前需要加载的数据 用于存储处理完后的历史数据。历史数据一般保存 3-6 个月后需要清除,以节省空间。但不同的项目要区别对待,如果源系统的数据量不大,可以保留更长的时间,甚至全量保存。 注意:在这层,理应不是简单的数据接入,而是要考虑一定的数据清洗,比如异常字段的处理、字段命名规范化、时间字段的统一等,一般这些很容易会被忽略,但是却至关重要。特别是后期我们做各种特征自动生成的时候,会十分有用。 注意:有的公司 ODS 层不会做太多数据过滤处理,会放到 DWD 层来处理。有的公司会在一开始时就在 ODS 层做数据相对精细化的过滤. 这个并没有明确规定, 看每个公司自己的想法和技术规范。 一般企业开发时,都会对原始数据存入到 ODS 时, 做一些最基本的处理。 数据来源区分 数据按照时间分区存储,一般是按照天, 也有公司使用年、月、日三级分区做存储的。 进行最基本的数据处理,如格式错误的丢弃,关键信息丢失的过滤掉等等。 数据实时离线 离线方面:每日定时任务型:跑批任务,业务库,比如我们典型的日计算任务,这里经常会使用 Sqoop 来抽取,比如我们每天定时抽取一次。每天凌晨算前一天的数据,早上起来看报表。这种任务经常使用 Hive、Spark 来计算,最终结果写入 Hive、Hbase、Mysql、Es 或者 Redis 中。 实时数据:日志埋点数据或者业务库,这部分主要是各种实时的系统使用,比如我们的实时推荐、实时用户画像,一般我们会用 Spark Streaming、Flink 来计算,最后会落入 Es、Hbase 或者 Redis 中。数据源是业务数据库,可以考虑用 Canal 监听 Mysql 的 Binlog,实时接入即可,然后也是收集到消息队列中,最终再由 Camus 拉取到 HDFS。 1)数据主要来源: 数据源是业务数据库,公司所有的系统产生的数据 是通过在客户端埋点上报,收集用户的行为日志,以及一些后端日志的日志类型数据源。对于埋点行为日志来说,一般会经过一个这样的流程,首先数据会上报到 Nginx 然后经过 Flume 收集,然后存储到 Kafka 这样的消息队列,然后再由实时或者离线的一些拉取的任务,拉取到我们的离线数据仓库 HDFS 外部数据(包括合作数据以及爬虫获得的数据),将所采集的数据汇总到一起 2)数据存储策略(增量、全量) 实际应用中,可以选择采用增量、全量存储或拉链存储的方式。 增量存储 为了满足历史数据分析需求,您可以在ODS层表中添加时间维度作为分区字段。以天为单位的增量存储,以业务日期作为分区,每个分区存放日增量的业务数据。 举例如下: 1月1日,用户A访问了A公司电商店铺B,A公司电商日志产生一条记录t1。1月2日,用户A又访问了A公司电商店铺C,A公司电商日志产生一条记录t2。 采用增量存储方式,t1将存储在1月1日这个分区中,t2将存储在1月2日这个分区中。 1月1日,用户A在A公司电商网购买了B商品,交易日志将生成一条记录t1。1月2日,用户A又将B商品退货了,交易日志将更新t1记录。 采用增量存储方式,初始购买的t1记录将存储在1月1日这个分区中,更新后的t1将存储在1月2日这个分区中。 交易、日志等事务性较强的ODS表适合增量存储方式。这类表数据量较大,采用全量存储的方式存储成本压力大。此外,这类表的下游应用对于历史全量数据访问的需求较小(此类需求可通过数据仓库后续汇总后得到)。例如,日志类ODS表没有数据更新的业务过程,因此所有增量分区UNION在一起就是一份全量数据。 全量存储 以天为单位的全量存储,以业务日期作为分区,每个分区存放截止到业务日期为止的全量业务数据。 例如,1月1日,卖家A在A公司电商网发布了B、C两个商品,前端商品表将生成两条记录t1、t2。1月2日,卖家A将B商品下架了,同时又发布了商品D,前端商品表将更新记录t1,同时新生成记录t3。采用全量存储方式,在1月1日这个分区中存储t1和t2两条记录,在1月2日这个分区中存储更新后的t1以及t2、t3记录。 对于小数据量的缓慢变化维度数据,例如商品类目,可直接使用全量存储。 拉链存储 拉链存储通过新增两个时间戳字段(start_dt和end_dt),将所有以天为粒度的变更数据都记录下来,通常分区字段也是这两个时间戳字段。 方案 概念:又称为接口层(stage),用于存储每天的增量数据和变更数据 数据生成方式:直接从kafka接收源数据,需要业务表每天生成update,delete,inseret数据,只生成insert数据的业务表,数据直接入明细层。 讨论方案:只把canal日志直接入缓冲层,如果其它有拉链数据的业务,也入缓冲层。 日志存储方式:使用impala外表,parquet文件格式,方便需要MR处理的数据读取。 日志删除方式:长久存储,可只存储最近几天的数据。讨论方案:直接长久存储。 表schema:一般按天创建分区,partitioned by 一般都是按照天进行存放。 库与表命名。库名:ods,表名:初步考虑格式为ods日期业务表名,待定。 hive的外部表,对应的是业务表。 hive外部表,存放数据的文件可以不是在hive的hdfs默认的位置,并且hive对应的表删除时,相应的数据文件并不会被删除.这样对于企业开发来说,可以防止因为删除表的操作而把宝贵的数据删除掉hive的业务表,则相反.数据文件存放在hive对应的默认位置,表删除时,对应文件也会被删除掉。 数仓层(DW,data warehouse) 数据仓库层(DW)层:数据仓库层是我们在做数据仓库时要核心设计的一层,本层将从 ODS 层中获得的数据按照主题建立各种数据模型,每一个主题对应一个宏观的分析领域,数据仓库层排除对决策无用的数据,提供特定主题的简明视图。在DW层会保存BI系统中所有的历史数据,例如保存10年的数据。 DW存放明细事实数据、维表数据及公共指标汇总数据。其中,明细事实数据、维表数据一般根据ODS层数据加工生成。公共指标汇总数据一般根据维表数据和明细事实数据加工生成。 DW层又细分为维度层(DIM)、明细数据层(DWD)和汇总数据层(DWS),采用维度模型方法作为理论基础,可以定义维度模型主键与事实模型中外键关系,减少数据冗余,也提高明细数据表的易用性。在汇总数据层同样可以关联复用统计粒度中的维度,采取更多的宽表化手段构建公共指标数据层,提升公共指标的复用性,减少重复加工。 维度层(DIM,Dimension):以维度作为建模驱动,基于每个维度的业务含义,通过添加维度属性、关联维度等定义计算逻辑,完成属性定义的过程并建立一致的数据分析维表。为了避免在维度模型中冗余关联维度的属性,基于雪花模型构建维度表。 明细数据层(DWD,Data Warehouse Detail):以业务过程作为建模驱动,基于每个具体的业务过程特点,构建最细粒度的明细事实表。可将某些重要属性字段做适当冗余,也即宽表化处理。 汇总数据层(DWS,Data Warehouse Summary):以分析的主题对象作为建模驱动,基于上层的应用和产品的指标需求,构建公共粒度的汇总指标表。以宽表化手段物理化模型,构建命名规范、口径一致的统计指标,为上层提供公共指标,建立汇总宽表、明细事实表。 主题域:面向业务过程,将业务活动事件进行抽象的集合,如下单、支付、退款都是业务过程。针对公共明细层(DWD)进行主题划分。 数据域:面向业务分析,将业务过程或者维度进行抽象的集合。针对公共汇总层(DWS)进行数据域划分。 DWD 层是以业务过程为驱动。 DWS 层、DWT 层和 ADS 层都是以需求为驱动。 DWD:data warehouse details 数据明细层。主要对ODS数据层做一些数据清洗和规范化的操作。 数据清洗:去除空值、脏数据、枚举值转换,超过极限范围的。 DWB:data warehouse base 数据基础层,存储的是客观数据,一般用作中间层,可以认为是大量指标的数据层。 DWS:data warehouse service 数据服务层,基于DWB上的基础数据,整合汇总成分析某一个主题域的服务数据层,一般是宽表。用于提供后续的业务查询,OLAP分析,数据分发等。 用户行为,轻度聚合 主要对ODS/DWD层数据做一些轻度的汇总。 1)公共维度层(DIM,Dimension) DIM:这一层比较单纯,举个例子就明白,比如国家代码和国家名、地理位置、中文名、国旗图片等信息就存在DIM层中。 基于维度建模理念思想,建立整个企业的一致性维度。降低数据计算口径和算法不统一风险。 公共维度汇总层(DIM)主要由维度表(维表)构成。维度是逻辑概念,是衡量和观察业务的角度。维表是根据维度及其属性将数据平台上构建的表物理化的表,采用宽表设计的原则。因此,构建公共维度汇总层(DIM)首先需要定义维度。 高基数维度数据:一般是用户资料表、商品资料表类似的资料表。数据量可能是千万级或者上亿级别。 低基数维度数据:一般是配置表,比如枚举值对应的中文含义,或者日期维表。数据量可能是个位数或者几千几万。 设计维表: 完成维度定义后,您就可以对维度进行补充,进而生成维表了。维表的设计需要注意: 建议维表单表信息不超过1000万条。 维表与其他表进行Join时,建议您使用Map Join 避免过于频繁的更新维表的数据。缓慢变化维:拉链表 公共维度汇总层(DIM)维表规范 公共维度汇总层(DIM)维表命名规范:dim_{业务板块名称/pub}_{维度定义}[_{自定义命名标签}],所谓pub是与具体业务板块无关或各个业务板块都可公用的维度,如时间维度。 例如:公共区域维表dim_pub_area 商品维表dim_asale_itm 事实表中一条记录所表达的业务细节程度被称为粒度。通常粒度可以通过两种方式来表述:一种是维度属性组合所表示的细节程度,一种是所表示的具体业务含义。通透!数据仓库领域常见建模方法及实例演示。 建模方式及原则 需要构建维度模型,一般采用星型模型,呈现的状态一般为星座模型(由多个事实表组合,维表是公共的,可被多个事实表共享); 为支持数据重跑可额外增加数据业务日期字段,可按日进行分表,用增量ODS层数据和前一天DWD相关表进行merge处理? 粒度是一行信息代表一次行为,例如一次下单。 维度建模步骤 选择业务过程:在业务系统中,挑选感兴趣的业务线,比如下单业务,支付业务,退款业务,物流业务,一条业务线对应一张事实表。如果是中小公司,尽量把所有业务过程都选择。DWD如果是大公司(1000多张表),选择和需求相关的业务线。 声明粒度:数据粒度指数据仓库的数据中保存数据的细化程度或综合程度的级别。声明粒度意味着精确定义事实表中的一行数据表示什么,应该尽可能选择最小粒度,以此来应各种各样的需求。典型的粒度声明如下:订单当中的每个商品项作为下单事实表中的一行,粒度为每次。每周的订单次数作为一行,粒度为每周。每月的订单次数作为一行,粒度为每月。如果在DWD层粒度就是每周或者每月,那么后续就没有办法统计细粒度的指标了。所以建议采用最小粒度。 确定维度:维度的主要作用是描述业务是事实,主要表示的是"谁,何处,何时"等信息。确定维度的原则是:后续需求中是否要分析相关维度的指标。例如,需要统计,什么时间下的订单多,哪个地区下的订单多,哪个用户下的订单多。需要确定的维度就包括:时间维度、地区维度、用户维度。维度表:需要根据维度建模中的星型模型原则进行维度退化。 确定事实:此处的"事实"一词,指的是业务中的度量值(次数、个数、件数、金额,可以进行累加),例如订单金额、下单次数等。在DWD层,以业务过程为建模驱动,基于每个具体业务过程的特点,构建最细粒度的明细层事实表。事实表可做适当的宽表化处理。 注意:DWD层是以业务过程为驱动。DWS层、DWT层和ADS层都是以需求为驱动,和维度建模已经没有关系了。DWS和DWT都是建宽表,按照主题去建表。主题相当于观察问题的角度。对应着维度表。 关于主题: 数据仓库中的数据是面向主题组织的,主题是在较高层次上将企业信息系统中的数据进行综合、归类和分析利用的一个抽象概念,每一个主题基本对应一个宏观的分析领域。如财务分析就是一个分析领域,因此这个数据仓库应用的主题就为"财务分析"。 关于主题域: 主题域通常是联系较为紧密的数据主题的集合。可以根据业务的关注点,将这些数据主题划分到不同的主题域(也说是对某个主题进行分析后确定的主题的边界) 关于主题域的划分: 主题域的确定必须由最终用户(业务)和数据仓库的设计人员共同完成的,而在划分主题域时,大家的切入点不同可能会造成一些争论、重构等的现象,考虑的点可能会是下方的某些方面: 按照业务或业务过程划分:比如一个靠销售广告位置的门户网站主题域可能会有广告域,客户域等,而广告域可能就会有广告的库存,销售分析、内部投放分析等主题; 根据需求方划分:比如需求方为财务部,就可以设定对应的财务主题域,而财务主题域里面可能就会有员工工资分析,投资回报比分析等主题; 按照功能或应用划分:比如微信中的朋友圈数据域、群聊数据域等,而朋友圈数据域可能就会有用户动态信息主题、广告主题等; 按照部门划分:比如可能会有运营域、技术域等,运营域中可能会有工资支出分析、活动宣传效果分析等主题; 总而言之,切入的出发点逻辑不一样,就可以存在不同的划分逻辑。在建设过程中可采用迭代方式,不纠结于一次完成所有主题的抽象,可先从明确定义的主题开始,后续逐步归纳总结成自身行业的标准模型。 主题:当事人、营销、财务、合同协议、机构、地址、渠道、产品、 金融业务主题有哪些:可分为四个主题: 用户主题(用户年龄、性别、收货地址、电话、省份等) 交易主题(订单数据、账单数据等) 风控主题(用户的风控等级,第三方征信数据) 营销主题(营销活动名单,活动配置信息等) 2)DWD(data warehouse detail)数据明细层,明细粒度事实层 DWD是业务层与数据仓库的隔离层,这一层主要解决一些数据质量问题和数据的完整度问题。 明细表用于存储ODS层原始表转换过来的明细数据,DWD 层的数据应该是一致的、准确的、干净的数据,即对源系统数据ODS层数据进行清洗(去除空值,脏数据,超过极限范围的数据,行式存储改为列存储,改压缩格式)、规范化、维度退化、脱敏等操作。比如用户的资料信息来自于很多不同表,而且经常出现延迟丢数据等问题,为了方便各个使用方更好的使用数据,我们可以在这一层做一个屏蔽。这一层也包含统一的维度数据。 明细粒度事实层(DWD):以业务过程作为建模驱动,基于每个具体的业务过程特点,构建最细粒度的明细层事实表。可以结合企业的数据使用特点,将明细事实表的某些重要维度属性字段做适当冗余,即宽表化处理。明细粒度事实层的表通常也被称为逻辑事实表。 负责数据的最细粒度的数据,在DWD层基础上,进行轻度汇总,结合常用维度(时间,地点,组织层级,用户,商品等) 该层一般保持和ODS层一样的数据粒度,并且提供一定的数据质量保证,在ODS的基础上对数据进行加工处理,提供更干净的数据。同时,为了提高数据明细层的易用性,该层会采用一些维度退化手法,当一个维度没有数据仓库需要的任何数据时,就可以退化维度,将维度退化至事实表中,减少事实表和维表的关联。 例如: 订单id,这种量级很大的维度,没必要用一张维度表来进行存储,而我们一般在进行数据分析时订单id又非常重要,所以我们将订单id冗余在事实表中,这种维度就是退化维度。 这一层的数据一般是遵循数据库第三范式或者维度建模,其数据粒度通常和 ODS 的粒度相同。在 PDW 层会保存 BI 系统中所有的历史数据,例如保存10年的数据。 数据在装入本层前需要做以下工作:去噪、去重、提脏、业务提取、单位统一、砍字段、业务判别。 清洗的数据种类: 不完整数据 错误数据 重复的数据 数据清洗的任务是过滤那些不符合要求的数据,将过滤的结果交给业务主管部门,确认是否过滤掉还是由业务单位修正之后再进行抽取。 DWD层做了哪些事? ①数据清洗过滤 去除废弃字段,去除格式错误的信息 去除丢失了关键字段的信息 过滤核心字段无意义的数据,比如订单表中订单id为null,支付表中支付id为空 对手机号、身份证号等敏感数据脱敏 去除不含时间信息的数据(这个看公司具体业务,但一般数据中都会带上时间戳,这样方便后续处理时,进行时间维度上信息分析处理和提取) 有些公司还会在这一层将数据打平,不过这具体要看业务需求.这是因为kylin适合处理展平后数据,不适合处理嵌套的表数据信息 有些公司还会将数据session做切割,这个一般是app的日志数据,其他业务场景不一定适合.这是因为app有进入后台模式,例如用户上午打开app用了10分钟,然后app切入后台,晚上再打开,这时候session还是一个,实际上应该做切割才对.(也有公司会记录app进入后台,再度进入前台的记录,这样来做session切割) ②数据映射,转换 将GPS经纬度转换为省市区详细地址。业界常见GPS快速查询一般将地理位置知识库使用geohash映射,然后将需要比对的GPS转换为geohash后跟知识库中geohash比对,查找出地理位置信息当然,也有公司使用open api,如高德地图,百度地图的api进行GPS和地理位置信息映射,但这个达到一定次数需要花钱,所以大家都懂的 会将IP地址也转换为省市区详细地址。这个有很多快速查找库,不过基本原理都是二分查找,因为ip地址可以转换为长整数.典型的如ip2region库 将时间转换为年,月,日甚至周,季度维度信息 数据规范化,因为大数据处理的数据可能来资源公司不同部门,不同项目,不同客户端,这时候可能相同业务数据字段,数据类型,空值等都不一样,这时候需要在DWD层做抹平.否则后续处理使用时,会造成很大的困扰 如boolean,有使用0 1标识,也有使用true false标识的 如字符串空值,有使用"",也有使用null,的,统一为null即可 如日期格式,这种就差异性更大,需要根据实际业务数据决定,不过一般都是格式化为YYYY-MM-dd HH:mm:ss 这类标准格式 维度退化:对业务数据传过来的表进行维度退化和降维。(商品一级二级三级、省市县、年月日)订单id冗余在事实表 清洗掉多少数据算合理:1万条数据清洗掉1条。 合理表数:一万张表变为三千张表,三千张表变为一千张表 明细粒度事实表设计原则: 一个明细粒度事实表仅和一个维度关联。 尽可能包含所有与业务过程相关的事实。 只选择与业务过程相关的事实。 分解不可加性事实为可加的组件。 在选择维度和事实之前必须先声明粒度。 在同一个事实表中不能有多种不同粒度的事实。 事实的单位要保持一致。粒度 谨慎处理Null值。 使用退化维度提高事实表的易用性。 方案 讨论方案:数据的合成方式为: 全量:每天把明细层的前天全量数据和昨天新数据合成一个新的数据表,覆盖旧表。同时使用历史镜像,按周/按月/按年存储一个历史镜像到新表。 日志存储方式:直接数据使用impala外表,parquet文件格式,建议使用内表,下面几层都是从impala生成的数据,建议都用内表+静态/动态分区。 表schema:一般按天创建分区,没有时间概念的按具体业务选择分区字段。partitioned by 一般都是按照天进行存放。 库与表命名。库名:dwd,表名:初步考虑格式为dwd日期业务表名,待定。 旧数据更新方式:直接覆盖 明细粒度事实层(DWD)规范 命名规范为:dwd_{业务板块/pub}_{数据域缩写}_{业务过程缩写}[_{自定义表命名标签缩写}] _{单分区增量全量标识},pub表示数据包括多个业务板块的数据。单分区增量全量标识通常为:i表示增量,f表示全量。 例如:dwd_asale_trd_ordcrt_trip_di(A电商公司航旅机票订单下单事实表,日刷新增量)dwd_asale_itm_item_df(A电商商品快照事实表,日刷新全量)。 本教程中,DWD层主要由三个表构成: 交易商品信息事实表:dwd_asale_trd_itm_di。 交易会员信息事实表:ods_asale_trd_mbr_di。 交易订单信息事实表:dwd_asale_trd_ord_di。 复制 CREATE TABLE IF NOT EXISTS dwd_asale_trd_itm_di ( item_id BIGINT COMMENT '商品ID', item_title STRING COMMENT '商品名称', item_price DOUBLE COMMENT '商品价格', item_stuff_status BIGINT COMMENT '商品新旧程度_0全新1闲置2二手', item_prov STRING COMMENT '商品省份', item_city STRING COMMENT '商品城市', cate_id BIGINT COMMENT '商品类目ID', cate_name STRING COMMENT '商品类目名称', commodity_id BIGINT COMMENT '品类ID', commodity_name STRING COMMENT '品类名称', buyer_id BIGINT COMMENT '买家ID', ) COMMENT '交易商品信息事实表' PARTITIONED BY (ds STRING COMMENT '日期') LIFECYCLE 400; 3)DWS(data warehouse service)数据服务层,汇总层宽表 基于 DWD 明细数据层,我们会按照一些分析场景、分析实体等去组织我们的数据,组织成一些分主题的汇总数据层 DWS。 明细粒度 ==> 汇总粒度 DWS层(数据汇总层)宽表,面向主题的汇总,维度相对来说比较少,DWS是根据DWD层基础数据按各个维度ID进行粗粒度汇总聚合,如按交易来源,交易类型进行汇合。整合汇总成分析某一个主题域的服务数据,一般是宽表。 以DWD为基础,按天进行轻度汇总。统计各个主题对象的当天行为,(例如,购买行为,统计商品复购率)。 该层数据表会相对比较少,大多都是宽表(一张表会涵盖比较多的业务内容,表中的字段较多)。按照主题划分,如订单、用户等,生成字段比较多的宽表,用于提供后续的业务查询,OLAP分析,数据分发等。 融合多个中间层数据,基于主题形成事实表,比如用户事实表、渠道事实表、终端事实表、资产事实表等等,事实表一般是宽表,在本层上实现企业级数据的一致性。 首先划分业务主题,将主题划分为销售域、库存域、客户域、采购域 等,其次就是 确定每个主题域的事实表和维度表。通常根据业务需求,划分成流量、订单、用户等,生成字段比较多的宽表,用于提供后续的业务查询,OLAP分析,数据分发等。 最近一天某个类目(例如:厨具)商品在各省的销售总额、该类目Top10销售额商品名称、各省用户购买力分布。因此,我们可以以最终交易成功的商品、类目、买家等角度对最近一天的数据进行汇总。 比如用户每个时间段在不同登录ip购买的商品数等。这里做一层轻度的汇总会让计算更加的高效,在此基础上如果计算仅7天、30天、90天的行为的话会快很多。我们希望80%的业务都能通过我们的DWS层计算,而不是ODS。 DWS层做了哪些事? dws将dwd层的数据按主题进行汇总,按照主题放到一个表中, 比如用户主题下会将用户注册信息、用户收货地址、用户的征信数据放到同一张表中,而这些在dwd层是对应多张表的,按照业务划分,如流量、订单、用户等,生成字段比较多的宽表 主题建模,围绕某一个业务主题进行数据建模,将相关数据抽离提取出来. 如: 将流量会话按照天,月进行聚合 将每日新用户进行聚合 将每日活跃用户进行聚合 维度建模,其实也差不多,不过是根据业务需要,提前将后续数据查询处理需要的维度数据抽离处理出来,方便后续查询使用. 如将运营位维度数据聚合 将渠道拉新维度数据聚合 ①DWS层每个主题1-3张宽表(处理100-200个指标 70%以上的需求) 具体宽表名称:用户行为宽表,用户购买商品明细行为宽表,商品宽表,物流宽表、售后等。 ②哪个宽表最宽?大概有多少个字段? 最宽的是用户行为宽表。大概有60-200个字段 ③具体用户行为宽表字段名称 评论、打赏、收藏、关注--商品、关注--人、点赞、分享、好价爆料、文章发布、活跃、签到、补签卡、幸运屋、礼品、金币、电商点击、gmv ④分析过的指标 日活、月活、周活、留存、留存率、新增(日、周、年)、转化率、流失、回流、七天内连续 3 天登录(点赞、收藏、评价、购买、加购、下单、活动)、连续 3 周(月)登录、GMV(成交金额,下单)、复购率、复购率排行、点赞、评论、收藏、领优惠价人数、使用优惠价、沉默、值不值得买、退款人数、退款率 topn 热门商品 活跃 日活:100 万;月活:是日活的 2-3 倍 300 万 总注册的用户多少?1000 万-3000 万之间 GMV,哪个商品卖的最好?每天下单量多少? GMV:每天 10 万订单(50–100 元)500 万-1000 万 100万的日活每天大概有10万人购买,平均每人消费100元,一天的GMV在1000万 10%-20% 100 万-200 万 复购率 某日常商品复购;(手纸、面膜、牙膏)10%-20% 电脑、显示器、手表 1% 转化率 商品详情 =》加购物车 =》下单 =》支付 5%-10% 60-70% 90%-95% 留存率 1/2/3、周留存、月留存 搞活动:10-20% 方案: 概念:又称数据集市或宽表。按照业务划分,如流量、订单、用户等,生成字段比较多的宽表,用于提供后续的业务查询,OLAP分析,数据分发等。 数据生成方式:由轻度汇总层和明细层数据计算生成。 日志存储方式:使用impala内表,parquet文件格式。 表schema:一般按天创建分区,没有时间概念的按具体业务选择分区字段。 库与表命名。库名:dws, 表名:初步考虑格式为:dws日期业务表名,待定。 旧数据更新方式:直接覆盖 公共汇总事实表规范 公共汇总事实表命名规范:dws_{业务板块缩写/pub}_{数据域缩写}_{数据粒度缩写}[_{自定义表命名标签缩写}]_{统计时间周期范围缩写}。关于统计实际周期范围缩写,缺省情况下,离线计算应该包括最近一天(_1d),最近N天(_nd)和历史截至当天(_td)三个表。如果出现_nd的表字段过多需要拆分时,只允许以一个统计周期单元作为原子拆分。即一个统计周期拆分一个表,例如最近7天(_1w)拆分一个表。不允许拆分出来的一个表存储多个统计周期。 对于小时表(无论是天刷新还是小时刷新),都用_hh来表示。对于分钟表(无论是天刷新还是小时刷新),都用_mm来表示。 举例如下: dws_asale_trd_byr_subpay_1d(买家粒度交易分阶段付款一日汇总事实表) dws_asale_trd_byr_subpay_td(买家粒度分阶段付款截至当日汇总表) dws_asale_trd_byr_cod_nd(买家粒度货到付款交易汇总事实表) dws_asale_itm_slr_td(卖家粒度商品截至当日存量汇总表) dws_asale_itm_slr_hh(卖家粒度商品小时汇总表)---维度为小时 dws_asale_itm_slr_mm(卖家粒度商品分钟汇总表)---维度为分钟 用户维度:用户主题 复制 drop table if exists dws_sale_detail_daycount; create external table dws_sale_detail_daycount( user_id string comment '用户 id', --用户信息 user_gender string comment '用户性别', user_age string comment '用户年龄', user_level string comment '用户等级', buyer_nick string comment '买家昵称', mord_prov string comment '地址', --下单数、 商品数量, 金额汇总 login_count bigint comment '当日登录次数', cart_count bigint comment '加入购物车次数', order_count bigint comment '当日下单次数', order_amount decimal(16,2) comment '当日下单金额', payment_count bigint comment '当日支付次数', payment_amount decimal(16,2) comment '当日支付金额', confirm_paid_amt_sum_1d double comment '最近一天订单已经确认收货的金额总和' order_detail_stats array<struct<sku_id:string,sku_num:bigint,order_count:bigint,order_amount:decimal(20,2)>> comment '下单明细统计' ) comment '每日购买行为' partitioned by(`dt` string) stored as parquet location '/warehouse/gmall/dws/dws_sale_detail_daycount/' tblproperties("parquet.compression" = "lzo"); 商品维度:商品主题 复制 CREATE TABLE IF NOT EXISTS dws_asale_trd_itm_ord_1d ( item_id BIGINT COMMENT '商品ID', --商品信息,产品信息 item_title STRING COMMENT '商品名称', cate_id BIGINT COMMENT '商品类目ID', cate_name STRING COMMENT '商品类目名称', --mord_prov STRING COMMENT '收货人省份', --商品售出金额汇总 confirm_paid_amt_sum_1d DOUBLE COMMENT '最近一天订单已经确认收货的金额总和' ) COMMENT '商品粒度交易最近一天汇总事实表' PARTITIONED BY (ds STRING COMMENT '分区字段YYYYMMDD') LIFECYCLE 36000; 问:数据集市层是不是没地方放了,各个业务的数据集市表是应该在 dws 还是在 app? 答:这个问题不太好回答,我感觉主要就是明确一下数据集市层是干什么的,如果你的数据集市层放的就是一些可以供业务方使用的宽表,放在 app 层就行。如果你说的数据集市层是一个比较泛一点的概念,那么其实 dws、dwd、app 这些合起来都算是数据集市的内容。 应用层(ADS)applicationData Service应用数据服务 数据应用层(ADS,Application Data Store):存放数据产品个性化的统计指标数据,报表数据。主要是提供给数据产品和数据分析使用的数据,通常根据业务需求,划分成流量、订单、用户等,生成字段比较多的宽表,用于提供后续的业务查询,OLAP分析,数据分发等。从数据粒度来说,这层的数据是汇总级的数据,也包括部分明细数据。从数据的时间跨度来说,通常是DW层的一部分,主要的目的是为了满足用户分析的需求,而从分析的角度来说,用户通常只需要分析近几年的即可。从数据的广度来说,仍然覆盖了所有业务数据。 在 DWS 之上,我们会面向应用场景去做一些更贴近应用的 APP 应用数据层,这些数据应该是高度汇总的,并且能够直接导入到我们的应用服务去使用。 应用层(ADS):应用层主要是各个业务方或者部门基于DWD和DWS建立的数据集市(Data Market, DM),一般来说应用层的数据来源于DW层,而且相对于DW层,应用层只包含部门或者业务方面自己关心的明细层和汇总层的数据。 该层主要是提供数据产品和数据分析使用的数据。一般就直接对接OLAP分析,或者业务层数据调用接口了 数据应用层APP:面向业务定制的应用数据主要提供给数据铲平和数据分析使用的数据,一般会放在ES,MYSQL,Oracle,Redis等系统供线上系统使用,也可以放在Hive 或者 Druid 中供数据分析和数据挖掘使用。 APP 层:为应用层,这层数据是完全为了满足具体的分析需求而构建的数据,也是星形或雪花结构的数据。如我们经常说的报表数据,或者说那种大宽表,一般就放在这里。包括前端报表、分析图表、KPI、仪表盘、OLAP、专题等分析,面向最终结果用户; 概念:应用层是根据业务需要,由前面三层数据统计而出的结果,可以直接提供查询展现,或导入至Mysql中使用。 数据生成方式:由明细层、轻度汇总层,数据集市层生成,一般要求数据主要来源于集市层。 日志存储方式:使用impala内表,parquet文件格式。 表schema:一般按天创建分区,没有时间概念的按具体业务选择分区字段。 库与表命名。库名:暂定ads,另外根据业务不同,不限定一定要一个库。 旧数据更新方式:直接覆盖。 ADS 层复购率统计 复制 CREATE TABLE app_usr_interact( user_id string COMMENT '用户id', nickname string COMMENT '用户昵称', register_date string COMMENT '注册日期', register_from string COMMENT '注册来源', remark string COMMENT '细分渠道', province string COMMENT '注册省份', pl_cnt bigint COMMENT '评论次数', ds_cnt bigint COMMENT '打赏次数', sc_add bigint COMMENT '添加收藏', sc_cancel bigint COMMENT '取消收藏', gzg_add bigint COMMENT '关注商品', gzg_cancel bigint COMMENT '取消关注商品', gzp_add bigint COMMENT '关注人', gzp_cancel bigint COMMENT '取消关注人', buzhi_cnt bigint COMMENT '点不值次数', zhi_cnt bigint COMMENT '点值次数', zan_cnt bigint COMMENT '点赞次数', share_cnts bigint COMMENT '分享次数', bl_cnt bigint COMMENT '爆料数', fb_cnt bigint COMMENT '好价发布数', online_cnt bigint COMMENT '活跃次数', checkin_cnt bigint COMMENT '签到次数', fix_checkin bigint COMMENT '补签次数', house_point bigint COMMENT '幸运屋金币抽奖次数', house_gold bigint COMMENT '幸运屋积分抽奖次数', pack_cnt bigint COMMENT '礼品兑换次数', gold_add bigint COMMENT '获取金币', gold_cancel bigint COMMENT '支出金币', surplus_gold bigint COMMENT '剩余金币', event bigint COMMENT '电商点击次数', gmv_amount bigint COMMENT 'gmv', gmv_sales bigint COMMENT '订单数' ) PARTITIONED BY( dt string) --stat_dt date COMMENT '互动日期', ①如何分析用户活跃? 在启动日志中统计不同设备 id 出现次数。 ②如何分析用户新增? 用活跃用户表 left join 用户新增表,用户新增表中 mid 为空的即为用户新增。 ③如何分析用户 1 天留存? 留存用户=前一天新增 join 今天活跃 用户留存率=留存用户/前一天新增 ④如何分析沉默用户? (登录时间为 7 天前,且只出现过一次) 按照设备 id 对日活表分组,登录次数为 1,且是在一周前登录。 ⑤如何分析本周回流用户? 本周活跃 left join 本周新增 left join 上周活跃,且本周新增 id 和上周活跃 id 都为 null。 ⑥如何分析流失用户? (登录时间为 7 天前) 按照设备 id 对日活表分组,且七天内没有登录过。 ⑦如何分析最近连续 3 周活跃用户数? 按照设备 id 对周活进行分组,统计次数大于 3 次。 ⑧如何分析最近七天内连续三天活跃用户数? 查询出最近 7 天的活跃用户,并对用户活跃日期进行排名 计算用户活跃日期及排名之间的差值 对同用户及差值分组,统计差值个数 将差值相同个数大于等于 3 的数据取出,然后去重,即为连续 3 天及以上活跃的用户 7 天连续收藏、点赞、购买、加购、付款、浏览、商品点击、退货 1 个月连续 7 天 连续两周 TMP:每一层的计算都会有很多临时表,专设一个DW TMP层来存储我们数据仓库的临时表。 层次调用规范 禁止反向调用 ODS 只能被 DWD 调用。 DWD 可以被 DWS 和 ADS 调用。 DWS 只能被 ADS 调用。 数据应用可以调用 DWD、DWS、ADS,但建议优先考虑使用汇总度高的数据。 ODS->DWD->DWS>ADS ODS->DWD->ADS
2024年02月19日
76 阅读
0 评论
0 点赞
2024-01-21
Elasticsearch DSL基本使用:详细教程和最佳实践
查询 使用 from 和 size 分页查询,需要注意的是:from+size 要小于 10000,不然会报错 // 从第9条开始,查询100条 GET index_test/_search?from=9&size=100 // 这个也行 GET index_test/_search { "from":9, "size":100 } 查询某个 id: DE7BEo0BngUdM_im08lv GET index_test/_doc/DE7BEo0BngUdM_im08lv 查询结果不要元字段,只查询 _source,使用 _source api 加上 id GET index_test/_source/2 一次性查询多个 id 值 GET index_test/_mget { "ids":[1,2] } GET index_test/_search { "query":{ "ids": { "values":[1,2,4] } } } match match 搜索需要注意以下几点: 被查询字段必须是可以分词的,比如 name 字段必须是可以分词的,例如 text 类型。 搜索过程是:首先对搜索词进行分词,然后只要一个某个分词和被搜索字段的分词匹配上了,就满足。、 es 分词的时候,首先会对分词统一改成小写,所以不区分大小写了。 例如下例子中,name 字段必须是可以分词的字段,然后搜索词 小米手机,会被分词 GET index_test/_search { "query":{ "match": { "name": "小米手机" } } } 搜索词分词的时候,本身也会作为分词结果之一,例如 HuaWei Mate 9000 会作为一个特殊项,作为整体去匹配 GET index_test/_search { "query":{ "match": { "name.keyword": "HuaWei Mate 9000" } } } match_phrase 这个查询和 match 查询一样,不同点在于所有分词要按顺序都匹配上 会对搜索词进行分词 搜索词的分词必须全部匹配上,并且顺序也要相同,中间不能有其他词。 中间不能有其它词,不过这个可以通过 slop 配置允许多少个其他词,默认是 0 个。 例如下例中:搜索词 xiao mi 会被分词成 xiao、mi 两个单词。这两个单词去 name 的分词中搜索,如果 name 中有个 xiao mi 既可以被匹配上,如果 name 是 xiao x mi 就不能匹配上,因为中间有其他干扰词语。 GET index_test/_search { "query":{ "match_phrase": { "name": "xiao mi" } } } 可以通过 slop 参数配置干扰词的数量,例如下例中,slop=1,这样 name 是 xiao x mi 就能匹配上了 GET index_test/_search { "query":{ "match_phrase": { "name": { "query": "小米手机", "slop": 1 } } } } term 一般用于精确查询 不会对搜索词进行分词,会保留搜索词的所有属性,包括大小写,标点符号等。 被搜索字段一般是子字段 keyword(text 类型一般都有子字段,叫做 keyword,记录着原始信息,不过文本内容如果太长的话,就会忽略,导致搜索不到),或者其他不分词的字段 被搜索是可以分词的字段也是可以,需要和分词结果匹配上就行 下面例子中,Xiao mi shou ji 会作为整体去匹配,name 必须也是 Xiao mi shou ji 才可以被匹配上 GET index_test/_search { "query":{ "term": { "name.keyword": { "value": "Xiao mi shou ji" } } } } 如果想要一次搜索多个词,可以用 terms GET index_test/_search { "query":{ "terms": { "name.keyword": ["xiao mi shou ji","hua wei shou ji"] } } } range 只支持大于,大于等于,小于,小于等于 GET index_test/_search { "query":{ "range": { "age":{ "gte":10, "lte":20 } } } } bool 组合 组合查询,就是多条件查询,bool 里的每个条件自己是 and 关系。 must must 查询,定义多个必须同时满足的条件 GET index_test/_search { "_source":fase, // 关掉 "query":{ "bool": { "must": [ // 多个条件必须同时满足 { "match": { "FIELD": "TEXT" }, "match_phrase": { "FIELD": "PHRASE" } } ] } } } filter filter 和 must 是一样的,只是会缓存,没有计算评分排序,如果不需要排序,用这个会快一点 GET index_test/_search { "query":{ "bool": { "filter": [ { "match": { "name": "小米" }, "range": { "price": { "lte": 20 } } } ] } } } should should 子句,和 or 有点类型,但是不等价 GET index_test/_search { "query":{ "bool": { "should": [ { "match": { "name": "小米" }, "range": { "price": { "lte": 20 } } } ], "minimum_should_match": 1 // should 条件应该满足的个数,默认就是1 } } } must not must not,子条件都不能满足,但凡满足一个都不行 GET index_test/_search { "query":{ "bool": { "must_not": [ { "match": { "name": "小米" }, "range": { "price": { "lte": 20 } } } ] } } } 嵌套子查询 嵌套子查询,需要再加上 bool。 下例中:name != iphone AND ((name = phone AND price <= 20) OR (type = phone AND brand IN ("苹果","华为"))) { "query": { "bool": { "must_not": [ { "match": { "name": "iphone" } } ], "should": [ { "bool": { "must": [ { "match": { "name": "phone" } }, { "range": { "price": { "lte": 20 } } } ] } }, { "bool": { "must": [ { "match": { "type": "phone" } }, { "terms": { "brand.keywords": [ "华为", "苹果" ] } } ] } } ] } } } 索引操作 因为,新增文档的时候,如果索引不存在也会自动创建,没有必要特意创建,了解就行 创建索引 index_test,请求体 body 可以没有。 PUT index_test { "settings":{ "number_of_shards":1,// 主分片数量 "number_of_replicas":1 // 每个主分片的副本分片数量 } } 修改索引 index_test PUT index_test/_settings { "number_of_shards":1 } 删除索引,这个操作要小心 DELETE index_test 新增文档 创建数据,1 是文档 id,如果已经存在就会报错 PUT index_test/_doc/1?op_type=create { "name":"手机", "content":"小米" } 等效 _create api PUT index_test/_create/2 { "name":"手机", "content":"小米" } 如果 id 已经存在,希望更新的话,可以使用 index,这个更新是覆盖掉原来的内容 PUT index_test/_doc/1?op_type=index { "name":"手机1", "content":"小米" } // 不加op_type=index 也一样 PUT index_test/_doc/1 { "name":"手机1", "content":"小米" } 创建文档的时候,不指定 id,系统自动生成 uuid POST index_test/_doc/ { "name":"手机1", "content":"小米" } 批量创建,使用 bulk 接口,批量创建,注意不能格式化,必须一行是命令,下一行是数据,中间不能有换行和空格。 POST /_bulk {"create":{"_index":"index_test","_id":5}} // 命令 {"name":"手机5","content":"123"} // 数据 {"create":{"_index":"index_test","_id":6}} {"name":"手机6","content":"123"} {"create":{"_index":"index_test","_id":7}} {"name":"手机7","content":"123"} // 已经存在的不报错 POST /_bulk {"index":{"_index":"index_test","_id":5}} {"name":"手机5","content":"123"} {"index":{"_index":"index_test","_id":6}} {"name":"手机6","content":"123"} {"index":{"_index":"index_test","_id":7}} {"name":"手机7","content":"123"} 删除文档 删除的时候,要加上文档 id DELETE index_test/_doc/DE7BEo0BngUdM_im08lv 批量删除 POST /_bulk {"delete":{"_index":"index_test","_id":5}} {"delete":{"_index":"index_test","_id":6}} 通过条件删除 POST /index_test/_delete_by_query { "query":{ "term": { "price": 500 } } } 更新文档 更新 name 字段,这个时候要加上 doc 属性,如果有多个字段,就把相应的字段新增上去就很 POST index_test/_update/1 { "doc":{ "name":"手机1" } } 批量更新 POST /_bulk {"update":{"_index":"index_test","_id":5}} {"doc":{"name":"手机55"}} {"update":{"_index":"index_test","_id":6}} {"doc":{"name":"手机66"}} mapping 操作 mapping 相当于 mysql 的表结构,定义索引的了 es 的数据结构等信息 GET index_test/_mapping es 的字段可以有多个类型,如果是文本类型,系统默认会创建一个 keword 类型,字段名称就叫做 keword text 类型,字段内容会被分词,不会区服大小写 keword 类型,不会被分词,用于精确查询,会区分大小写,字段太长的时候会被忽略会搜索不到 nested 类型,字段是个 json 对象,或者是个数组里嵌套对象,这种比较复杂的类型 { "index_test": { "mappings": { "properties": { "content": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "price": { "type": "long" } } } } } 不像 mysql 使用表之前,需要先定义好表结构,es 添加数据的时候,可以新增字段,会自动传教 mapping,es 会更加值类型,创建相应的字段类型 PUT index_test/_create/10 { "name":"手机", "content":"小米2", "price":500 // 例如此时没有price字段 } 虽然 mapping 可以自动创建,不过最好手动创建,只能创建索引的时候设置好,后面很多参数都不允许修改 PUT index_test1 { "mappings":{ "properties":{ "long_field":{ "type":"long" } } } } 测试分词 查看分词结果 GET index_test/_analyze { "text":["华为mate"] }
2024年01月21日
107 阅读
0 评论
0 点赞
2024-01-20
MySQL Interval 用法详解 | MySQL 时间区间使用指南
interval 作为函数时 当 interval 作为一个函数时,它被当做一个比较函数,即 interval (),如 interval (4,0,1,2,3,4,5,6),则在函数中,第一个数 4 作为被比较数,后面的 0,1,2,3,4,5,6 为比较数,然后将后面的数字依次与 4 进行比较,返回小于等于 4 的个数,所以上述结果为 5,注意,只有将 4 后面的数字从小到大进行排列,interval 函数才能正常使用,若排序混乱,可以使用,但会影响最终结果。 interval 作为关键字时 当 interval 作为一个关键字时,表示为时间间隔,常用在 date_add ()、date_sub ()函数中,常用于时间的加减法。 时间的加法 使用 date_add 函数时 mysql> select date_add('2020-07-20', interval +1 day); +-----------------------------------------+ | date_add('2020-07-20', interval +1 day) | +-----------------------------------------+ | 2020-07-21 | +-----------------------------------------+ 1 row in set (0.01 sec) mysql> select date_add('2020-07-20', interval +1 hour); +------------------------------------------+ | date_add('2020-07-20', interval +1 hour) | +------------------------------------------+ | 2020-07-20 01:00:00 | +------------------------------------------+ 1 row in set (0.00 sec) mysql> select date_add('2020-07-20', interval +1 minute); +--------------------------------------------+ | date_add('2020-07-20', interval +1 minute) | +--------------------------------------------+ | 2020-07-20 00:01:00 | +--------------------------------------------+ 1 row in set (0.00 sec) 使用 DATE 关键字时,需要确定时间的粒度大小,再进行具体时刻的选取,可供选择的时间粒度如下 MICROSECOND SECOND MINUTE HOUR DAY WEEK MONTH QUARTER YEAR SECOND_MICROSECOND MINUTE_MICROSECOND MINUTE_SECOND HOUR_MICROSECOND HOUR_SECOND HOUR_MINUTE DAY_MICROSECOND DAY_SECOND DAY_MINUTE DAY_HOUR YEAR_MONTH 使用如下 mysql> select DATE '2020-07-20' + interval '1 31' hour_second; +-------------------------------------------------+ | DATE '2020-07-20' + interval '1 31' hour_second | +-------------------------------------------------+ | 2020-07-20 00:01:31 | +-------------------------------------------------+ 1 row in set (0.00 sec) mysql> select DATE '2020-07-20' + interval '2' microsecond; +----------------------------------------------+ | DATE '2020-07-20' + interval '2' microsecond | +----------------------------------------------+ | 2020-07-20 00:00:00.000002 | +----------------------------------------------+ 1 row in set (0.00 sec) 时间的减法 与时间的加法使用方式一致,将 + 号换成 - 号就可以 mysql> select date_add('2020-07-20', interval -1 day); +-----------------------------------------+ | date_add('2020-07-20', interval -1 day) | +-----------------------------------------+ | 2020-07-19 | +-----------------------------------------+ 1 row in set (0.00 sec) mysql> select date_add('2020-07-20', interval -1 hour); +------------------------------------------+ | date_add('2020-07-20', interval -1 hour) | +------------------------------------------+ | 2020-07-19 23:00:00 | +------------------------------------------+ 1 row in set (0.00 sec) mysql> select date_add('2020-07-20', interval -1 minute); +--------------------------------------------+ | date_add('2020-07-20', interval -1 minute) | +--------------------------------------------+ | 2020-07-19 23:59:00 | +--------------------------------------------+ 1 row in set (0.00 sec) ----------------- mysql> select DATE '2020-07-20' - interval '1 31' hour_second; +-------------------------------------------------+ | DATE '2020-07-20' - interval '1 31' hour_second | +-------------------------------------------------+ | 2020-07-19 23:58:29 | +-------------------------------------------------+ 1 row in set (0.00 sec) mysql> select DATE '2020-07-20' - interval '2' microsecond; +----------------------------------------------+ | DATE '2020-07-20' - interval '2' microsecond | +----------------------------------------------+ | 2020-07-19 23:59:59.999998 | +----------------------------------------------+ 1 row in set (0.00 sec)
2024年01月20日
91 阅读
0 评论
0 点赞
2024-01-03
Go 语言接口语法指南 | 进阶接口语法学习
概述 接口主要是用于实现多态的效果,接口是一个类型,一个抽象的类型,既然是类型,就可以定义接口类型的变量。 定义 接口好像用于定义方法的,写上方法签名列表即可 type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 // 签名 方法名2( 参数列表2 ) 返回值列表2 … } 接口名称:我们一般会在接口名称后面添加 er。 方法签名: 当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问,也就是说可以被其它包所实现; 参数列表和返回值列表中的参数 变量名可以省略 type sayer interface { Say() string } 结构体变量赋值 如果一个变量定义为接口类型的变量,那么如果一个结构体实现了这个接口,并且方法的接收者都是值类型的,那么这个结构体的值类型和指针类型都可以赋予给这个接口变量。 例如下例中,结构体 dog,实现了 Mover 接口的 move 和 eat 方法,并且这个两个方法的接受者都是值类型,所以 dog{} 可以赋值给这个 Mover 接口变量,&dog{} 也可以赋值给这个 Mover 接口变量 package main import "fmt" type Mover interface { move() eat() } // dog 类型的方法的接受者都是值类型 type dog struct{} func (d dog) move() { fmt.Println("狗会动") } func (d dog) eat() { fmt.Println("吃") } func main() { var x Mover x = &dog{} // 指针类型可以赋值 x.move() var y Mover y = dog{} // 值类型可以赋值 y.move() } 但是,如果结构体方法的接收者存在指针类型,那么只能这个结构体的指针类型都可以赋予给这个接口变量,值类型不行,会报错。 如下所示 move 方法是指针类型,所以只能 &dog{} 赋予给 Mover 接口变量,用 dog{} 值类型赋予接口变量的话,将会报错 package main import "fmt" type Mover interface { move() eat() } type dog struct{} // move 方法的接受者是指针类型*dog func (d *dog) move() { fmt.Println("狗会动") } func (d dog) eat() { fmt.Println("吃") } func main() { var x Mover x = &dog{} // 指针类型可以赋值 x.move() var y Mover y = dog{} // 值类型不可以赋值了,会报错!!! y.move() } 空接口 接口是行为规范的集合,空接口没有任务行为规范,也就是接口方法为空。因为没有任何行为规范,所以任何类型都可以属于空接口类型。所以空接口可以接收任何参数。 package main import "fmt" // Mover 自定义空接口 type Mover interface { } // dog 空接口参数arg func dog(arg Mover) { } func main() { // 空接口可以接受任何参数 dog(10) dog("10") // 空接口可以接受任何参数 var a Mover a = 10 a = "10" // Println 参数类型就是空接口 fmt.Println(a) } go 里其实已经自带了一个空接口类型 interface{},不需要我们自定义(上例的 Mover 空接口) package main import "fmt" func main() { // go里自带的空接口类型interface{} var a interface{} a = 10 a = "10" // Println 参数类型就是空接口 fmt.Println(a) } 在 go1.18 之后,官方更加建议使用 any 来替代 interface{},这样可以少写几个字母,没有别的差别。 package main import "fmt" func main() { // go1.18之后,官方推荐用any替代interface{} var a any a = 10 a = "10" // Println 参数类型就是空接口 fmt.Println(a) } 断言 由于空接口可以存任何类型的值,有时候我们想要知道,空接口类型的值到底是什么类型,这个时候就需要对值进行断言。 package main import "fmt" func main() { // 定义一个空接口x var x interface{} x = "Hello 沙河" v, ok := x.(string) // 断言是否是字符串,ok为true或false,v为变量的值 fmt.Println(v, ok) } 断言配合switch使用 anyVal.(type)的形式必须配合switch使用 var anyVal interface{} anyVal = "123" switch specificTypeVal := anyVal.(type) { case string: fmt.Println(specificTypeVal) // 输出字符串3 } 有时候我们希望结构体必须实现某个接口,如果没有实现的话,就编译报错,启动不起来,可以这样写 package main import ( "io" ) type MyStruct struct{} func (m *MyStruct) Read(p []byte) (n int, err error) { return 0, nil } // Impl 或者写个方法,返回这个接口类型也可以 func (m *MyStruct) Impl() io.Reader { return (*MyStruct)(nil) } // 这个两种方式,方法的接受者可以是指针,也可以是值类型 var _ io.Reader = (*MyStruct)(nil) // 把nil转成*MyStruct类型 var _ io.Reader = &MyStruct{} type MyStructCaseTwo struct{} func (m MyStructCaseTwo) Read(p []byte) (n int, err error) { return 0, nil } // 方法的接受这必须是指针类型 var _ io.Reader = MyStructCaseTwo{} func main() { } 接口的嵌套 接口与接口间可以通过嵌套创造出新的接口,嵌套得到的接口的使用与普通接口一样 // Sayer 接口 type Sayer interface { say() } // Mover 接口 type Mover interface { move() } // 接口嵌套 type animal interface { Sayer Mover } 接口的实现 接口是隐式实现的,一个对象只要全部实现了接口中的方法,那么就实现了这个接口;换句话说,接口就是一个 需要实现的方法列表。 type sayer interface { Say() string } type Cat struct{} // 实现了sayer接口 func (c Cat) Say() string { return "喵喵喵" } 一个类型可以实现多个接口,只需要实现每个接口里的方法即可 type Sayer interface { say() } type Mover interface { move() } type dog struct { name string } func (d dog) say() { fmt.Printf("%s会叫汪汪汪\n", d.name) } func (d dog) move() { fmt.Printf("%s会动\n", d.name) } func main() { var x Sayer var y Mover a := dog{name: "旺财"} x = a y = a x.say() y.move() } 还可以多个类型实现同一个接口,这就是多态的实现,同一个接口类型,不同对象有不同的表现,如下所示,同一个 move 方法,dog 结构体实例和 car 结构体实例,执行的结果是不一样的 type Mover interface { move() } type dog struct { name string } type car struct { brand string } func (d dog) move() { fmt.Printf("%s会跑\n", d.name) } func (c car) move() { fmt.Printf("%s速度70迈\n", c.brand) } func main() { var x Mover x = dog{name: "旺财"} x.move() x = car{brand: "保时捷"} x.move() }
2024年01月03日
61 阅读
0 评论
0 点赞
2023-12-31
Go语言泛型指南| Go语言泛型教程
从语法的角度来讲,泛型是从接口层面演化而来的。 下面例子中,f1 是依赖于接口类型 Man,f2 是依赖于 T 类型,T 类型等价于 Man 类型。 package main type Man interface { Say() } type Student struct { } func (s Student) Say() { } // 面向接口编程,a必须是Man接口类型 func f1(a Man) { } // 泛型,中括号里可以理解为T是Man的别名 // 既然T等Man,所以f1和f2就等价了 func f2[T Man](a T) { } func main() { f1(Student{}) f2(Student{}) } 应用在函数里 跟接口的语法有点类似,就是里面的内容是数据类型,而不是方法签名,如下所示 Happy 就是联合类型,将来要使用到 Happy 的时候,只需要用到任意一个类型传递进去就可以。 // 联合类型 type Happy interface { // ~T,是go1.18新增的符号,~t表示底层是T的所有类型 int | ~int32 | int8 | string | ~bool } // a,b必须同时为int、~int32、int8、string、 ~bool // 不可以一个是int,一个是string func f3[T Happy](a, b T, c float64) { } func main() { // a,b都是int f3(1, 2, 3.1) f3(true, false, 3.1) f3("a", "b", 3.1) } 如果不想要定义个结构体,可以如下所示,当然,a, b 两个参数每次调用的时候,也要是同一个类型。 package main func f4[T int | string | bool](a, b T, c float64) { } func main() { // a,b都是int f4(1, 1, 3.1) f4(true, false, 3.1) f4("a", "b", 3.1) } 应用在结构体 在接口体名称后面新增一个中括号,在中括号里设置好别名 package main import "fmt" type Happy interface { // ~T,是go1.18新增的符号,~t表示底层是T的所有类型 int | ~int32 | int8 | string | ~bool } type Bird[T Happy, S int | string] struct { Head T Hair S } func (b *Bird[T, S]) SetHead(h T) { b.Head = h } func main() { // [int, string],定下来[T, S]是什么 bird := Bird[int, string]{ Head: 3, Hair: "lang", } fmt.Println(bird.Head, bird.Head) }
2023年12月31日
106 阅读
0 评论
0 点赞
2023-12-30
深入理解Go语言的panic和recover机制
go 的 http 服务会一直在监听某个端口,来监听客户端发过来的请求,并且要为每个请求开辟一个单独的协程。这样看起来每个协程都是独立的,不不影响。 但是,如果在某个子协程里,发生了 panic 异常,会导致服务端进程退出,从而发生严重的线上故障。 例如下面服务,将引发 panic,当然引发 panic 的还有除以 0 等操作: package main import ( "time" ) func main() { go func() { var info map[string]string // 对未初始化的map进行赋值,panic info["a"] = "" }() for { time.Sleep(time.Second) } } 当引发 panic 时候,panic 会引发三件事情: 逆序执行当前 goroutine 的 defer 链(recover 从这里介入)。 打印错误信息和调用堆栈 调用 exit 结束整个进程 所以,我们可以在 defer 里使用 recover 函数,来阻断后面两步的执行,例如: package main import ( "fmt" "time" ) func main() { go func() { defer func() { if err := recover(); err != nil { // 输出:assignment to entry in nil map fmt.Println(err) } }() var info map[string]string // 对未初始化的map进行赋值,panic info["a"] = "" }() for { time.Sleep(time.Second) } } 触发 panic 的场景总结 数组越界的时候 s := []int{1, 2, 3} for i := 0; i <= 4; i++ { fmt.Println(s[i]) } 在实际开发中,也可以主动调用 panic 函数达到同样效果。 func panicFunc() { panic(errors.New("this is test for panic")) } 对未初始化的map进行赋值的时候 var info map[string]string // 对未初始化的map进行赋值,panic info["a"] = "" 空指针的时候
2023年12月30日
115 阅读
0 评论
0 点赞
2023-12-30
Go语言接口超时管理及优化
服务端在调用第三方接口有可能会超时,如果超时了,一般不会让客户端一直等,要设置最大响应时间,如果超过这个时间的话,就返回。 代码如下所示: package main import ( "net/http" "time" ) func readDb() string { // 200 ms time.Sleep(200 * time.Millisecond) return "OK" } func home(w http.ResponseWriter, req *http.Request) { var resp string // 容量设置1,类型直接设置空结构体即可 done := make(chan struct{}, 1) go func() { resp = readDb() done <- struct{}{} }() // 阻塞在这里,哪个先返回,就那个解除阻塞 select { case <-done: case <-time.After(300 * time.Millisecond): // 100ms 超时 resp = "timeout" } _, _ = w.Write([]byte(resp)) } func main() { http.HandleFunc("/", home) _ = http.ListenAndServe("127.0.0.1:5678", nil) }
2023年12月30日
112 阅读
0 评论
0 点赞
2023-12-30
TCP协议详解 | 网络通信的核心技术
tcp概述 TCP(Transmission Control Protocol)又叫传输控制协议,是面向连接的、可靠的、基于字节流的传输层通信协议。 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的; 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端; 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。 优点: 可靠,稳定。TCP的可靠性体现在传输数据之前,三次握手建立连接(四次挥手断开连接),并且在数据传递时,有确认,窗口,重传,拥塞控制机制,数据传完之后断开连接来节省系统资源。 应用场景: 对网络通信质量有要求时,比如:整个数据要准确无误的传递给对方,这往往对于一些要求可靠的应用,比如HTTP,HTTPS,FTP等传输文件的协议,POP,SMTP等邮件的传输协议,websocket协议等。 头部格式 我们先来看看 TCP 头的格式,标注颜色的表示与本文关联比较大的字段,其他字段不做详细阐述 序列号: 在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。 确认应答号: 指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。 控制位: ACK(Acknowledge character即是确认字符):该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。 RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。 SYN(同步序列编号Synchronize Sequence Numbers):该位为 1 时,表示希望建立连接,需要同步序列号,并在其「序列号」的字段进行序列号初始值的设定。 FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。 3次握手4次挥手 一个可靠连接肯定会有以下三个过程:创建连接、 数据传输、 终止连接 创建连接 这里就是常说的‘三次握手’,其实完全可以用常识来理解这个过程,两个人之间如果想建立一个通信,至少需要有三次对话才能保证通信的可靠。举个栗子,相信大家打游戏的时候都跟队友语音过。 {timeline} {timeline-item color="#19be6b"} A:听得到吗? 建立连接,客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认 {/timeline-item} {timeline-item color="#ed4014"} B:我听到了,你能听到我说话吗? 服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态; {/timeline-item} {timeline-item color="#ed4014"} A:ok,我能听到~ 客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。 {/timeline-item} {/timeline} 数据传输 {timeline} {timeline-item color="#19be6b"} A:我给你发个一堆东西,你按照顺序拼好 {/timeline-item} {timeline-item color="#ed4014"} B:收到了 发送的文件其实被拆成一个一个的小块,seq(Sequence Number)的作用是序列号,让服务器端能拼回来。 {/timeline-item} {/timeline} 终止连接 当数据传输完毕之后,就要终止连接了,也就是 ‘四次挥手’ {timeline} {timeline-item color="#19be6b"} A:我要下了,你有什么想对我说的吗? 主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了; {/timeline-item} {timeline-item color="#ed4014"} B:等等,让我说完 xxxxx…… 主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2同意‘关闭’,但是要等一下,因为这里可能还有数据没有接受完 {/timeline-item} {timeline-item color="#ed4014"} B:好了,我说完了。 主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态; {/timeline-item} {timeline-item color="#ed4014"} A:那拜拜啦~ 主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。 {/timeline-item} {/timeline}
2023年12月30日
107 阅读
0 评论
0 点赞
1
2
...
7