首页 文章 精选 留言 我的

精选列表

搜索[流式渲染],共10001篇文章
优秀的个人博客,低调大师

Go 通过 Map/Filter/ForEach 等流式 API 高效处理数据

什么是流处理 如果有 java 使用经验的同学一定会对 java8 的 Stream 赞不绝口,极大的提高了们对于集合类型数据的处理能力。 int sum = widgets.stream() .filter(w -> w.getColor() == RED) .mapToInt(w -> w.getWeight()) .sum(); Stream 能让我们支持链式调用和函数编程的风格来实现数据的处理,看起来数据像是在流水线一样不断的实时流转加工,最终被汇总。Stream 的实现思想就是将数据处理流程抽象成了一个数据流,每次加工后返回一个新的流供使用。 Stream 功能定义 动手写代码之前,先想清楚,把需求理清楚是最重要的一步,我们尝试代入作者的视角来思考整个组件的实现流程。首先把底层实现的逻辑放一下 ,先尝试从零开始进行功能定义 stream 功能。 Stream 的工作流程其实也属于生产消费者模型,整个流程跟工厂中的生产流程非常相似,尝试先定义一下 Stream 的生命周期: 创建阶段/数据获取(原料) 加工阶段/中间处理(流水线加工) 汇总阶段/终结操作(最终产品) 下面围绕 stream 的三个生命周期开始定义 API: 创建阶段 为了创建出数据流 stream 这一抽象对象,可以理解为构造器。 我们支持三种方式构造 stream,分别是:切片转换,channel 转换,函数式转换。 注意这个阶段的方法都是普通的公开方法,并不绑定 Stream 对象。 // 通过可变参数模式创建 stream func Just(items ...interface{}) Stream // 通过 channel 创建 stream func Range(source <-chan interface{}) Stream // 通过函数创建 stream func From(generate GenerateFunc) Stream // 拼接 stream func Concat(s Stream, others ...Stream) Stream 加工阶段 加工阶段需要进行的操作往往对应了我们的业务逻辑,比如:转换,过滤,去重,排序等等。 这个阶段的 API 属于 method 需要绑定到 Stream 对象上。 结合常用的业务场景进行如下定义: // 去除重复item Distinct(keyFunc KeyFunc) Stream // 按条件过滤item Filter(filterFunc FilterFunc, opts ...Option) Stream // 分组 Group(fn KeyFunc) Stream // 返回前n个元素 Head(n int64) Stream // 返回后n个元素 Tail(n int64) Stream // 转换对象 Map(fn MapFunc, opts ...Option) Stream // 合并item到slice生成新的stream Merge() Stream // 反转 Reverse() Stream // 排序 Sort(fn LessFunc) Stream // 作用在每个item上 Walk(fn WalkFunc, opts ...Option) Stream // 聚合其他Stream Concat(streams ...Stream) Stream 加工阶段的处理逻辑都会返回一个新的 Stream 对象,这里有个基本的实现范式 汇总阶段 汇总阶段其实就是我们想要的处理结果,比如:是否匹配,统计数量,遍历等等。 // 检查是否全部匹配 AllMatch(fn PredicateFunc) bool // 检查是否存在至少一项匹配 AnyMatch(fn PredicateFunc) bool // 检查全部不匹配 NoneMatch(fn PredicateFunc) bool // 统计数量 Count() int // 清空stream Done() // 对所有元素执行操作 ForAll(fn ForAllFunc) // 对每个元素执行操作 ForEach(fn ForEachFunc) 梳理完组件的需求边界后,我们对于即将要实现的 Stream 有了更清晰的认识。在我的认知里面真正的架构师对于需求的把握以及后续演化能达到及其精准的地步,做到这一点离不开对需求的深入思考以及洞穿需求背后的本质。通过代入作者的视角来模拟复盘整个项目的构建流程,学习作者的思维方法论这正是我们学习开源项目最大的价值所在。 好了,我们尝试定义出完整的 Stream 接口全貌以及函数。 > 接口的作用不仅仅是模版作用,还在于利用其抽象能力搭建项目整体的框架而不至于一开始就陷入细节,能快速的将我们的思考过程通过接口简洁的表达出来,学会养成自顶向下的思维方法从宏观的角度来观察整个系统,一开始就陷入细节则很容易拔剑四顾心茫然。。。 rxOptions struct { unlimitedWorkers bool workers int } Option func(opts *rxOptions) // key生成器 //item - stream中的元素 KeyFunc func(item interface{}) interface{} // 过滤函数 FilterFunc func(item interface{}) bool // 对象转换函数 MapFunc func(intem interface{}) interface{} // 对象比较 LessFunc func(a, b interface{}) bool // 遍历函数 WalkFunc func(item interface{}, pip chan<- interface{}) // 匹配函数 PredicateFunc func(item interface{}) bool // 对所有元素执行操作 ForAllFunc func(pip <-chan interface{}) // 对每个item执行操作 ForEachFunc func(item interface{}) // 对每个元素并发执行操作 ParallelFunc func(item interface{}) // 对所有元素执行聚合操作 ReduceFunc func(pip <-chan interface{}) (interface{}, error) // item生成函数 GenerateFunc func(source <-chan interface{}) Stream interface { // 去除重复item Distinct(keyFunc KeyFunc) Stream // 按条件过滤item Filter(filterFunc FilterFunc, opts ...Option) Stream // 分组 Group(fn KeyFunc) Stream // 返回前n个元素 Head(n int64) Stream // 返回后n个元素 Tail(n int64) Stream // 获取第一个元素 First() interface{} // 获取最后一个元素 Last() interface{} // 转换对象 Map(fn MapFunc, opts ...Option) Stream // 合并item到slice生成新的stream Merge() Stream // 反转 Reverse() Stream // 排序 Sort(fn LessFunc) Stream // 作用在每个item上 Walk(fn WalkFunc, opts ...Option) Stream // 聚合其他Stream Concat(streams ...Stream) Stream // 检查是否全部匹配 AllMatch(fn PredicateFunc) bool // 检查是否存在至少一项匹配 AnyMatch(fn PredicateFunc) bool // 检查全部不匹配 NoneMatch(fn PredicateFunc) bool // 统计数量 Count() int // 清空stream Done() // 对所有元素执行操作 ForAll(fn ForAllFunc) // 对每个元素执行操作 ForEach(fn ForEachFunc) } channel() 方法用于获取 Stream 管道属性,因为在具体实现时我们面向的是接口对象所以暴露一个私有方法 read 出来。 // 获取内部的数据容器channel,内部方法 channel() chan interface{} 实现思路 功能定义梳理清楚了,接下来考虑几个工程实现的问题。 如何实现链式调用 链式调用,创建对象用到的 builder 模式可以达到链式调用效果。实际上 Stream 实现类似链式的效果原理也是一样的,每次调用完后都创建一个新的 Stream 返回给用户。 // 去除重复item Distinct(keyFunc KeyFunc) Stream // 按条件过滤item Filter(filterFunc FilterFunc, opts ...Option) Stream 如何实现流水线的处理效果 所谓的流水线可以理解为数据在 Stream 中的存储容器,在 go 中我们可以使用 channel 作为数据的管道,达到 Stream 链式调用执行多个操作时异步非阻塞效果。 如何支持并行处理 数据加工本质上是在处理 channel 中的数据,那么要实现并行处理无非是并行消费 channel 而已,利用 goroutine 协程、WaitGroup 机制可以非常方便的实现并行处理。 go-zero 实现 core/fx/stream.go go-zero 中关于 Stream 的实现并没有定义接口,不过没关系底层实现时逻辑是一样的。 为了实现 Stream 接口我们定义一个内部的实现类,其中 source 为 channel 类型,模拟流水线功能。 Stream struct { source <-chan interface{} } 创建 API channel 创建 Range 通过 channel 创建 stream func Range(source <-chan interface{}) Stream { return Stream{ source: source, } } 可变参数模式创建 Just 通过可变参数模式创建 stream,channel 写完后及时 close 是个好习惯。 func Just(items ...interface{}) Stream { source := make(chan interface{}, len(items)) for _, item := range items { source <- item } close(source) return Range(source) } 函数创建 From 通过函数创建 Stream func From(generate GenerateFunc) Stream { source := make(chan interface{}) threading.GoSafe(func() { defer close(source) generate(source) }) return Range(source) } 因为涉及外部传入的函数参数调用,执行过程并不可用因此需要捕捉运行时异常防止 panic 错误传导到上层导致应用崩溃。 func Recover(cleanups ...func()) { for _, cleanup := range cleanups { cleanup() } if r := recover(); r != nil { logx.ErrorStack(r) } } func RunSafe(fn func()) { defer rescue.Recover() fn() } func GoSafe(fn func()) { go RunSafe(fn) } 拼接 Concat 拼接其他 Stream 创建一个新的 Stream,调用内部 Concat method 方法,后文将会分析 Concat 的源码实现。 func Concat(s Stream, others ...Stream) Stream { return s.Concat(others...) } 加工 API 去重 Distinct 因为传入的是函数参数KeyFunc func(item interface{}) interface{}意味着也同时支持按照业务场景自定义去重,本质上是利用 KeyFunc 返回的结果基于 map 实现去重。 函数参数非常强大,能极大的提升灵活性。 func (s Stream) Distinct(keyFunc KeyFunc) Stream { source := make(chan interface{}) threading.GoSafe(func() { // channel记得关闭是个好习惯 defer close(source) keys := make(map[interface{}]lang.PlaceholderType) for item := range s.source { // 自定义去重逻辑 key := keyFunc(item) // 如果key不存在,则将数据写入新的channel if _, ok := keys[key]; !ok { source <- item keys[key] = lang.Placeholder } } }) return Range(source) } 使用案例: // 1 2 3 4 5 Just(1, 2, 3, 3, 4, 5, 5).Distinct(func(item interface{}) interface{} { return item }).ForEach(func(item interface{}) { t.Log(item) }) // 1 2 3 4 Just(1, 2, 3, 3, 4, 5, 5).Distinct(func(item interface{}) interface{} { uid := item.(int) // 对大于4的item进行特殊去重逻辑,最终只保留一个>3的item if uid > 3 { return 4 } return item }).ForEach(func(item interface{}) { t.Log(item) }) 过滤 Filter 通过将过滤逻辑抽象成 FilterFunc,然后分别作用在 item 上根据 FilterFunc 返回的布尔值决定是否写回新的 channel 中实现过滤功能,实际的过滤逻辑委托给了 Walk method。 Option 参数包含两个选项: unlimitedWorkers 不限制协程数量 workers 限制协程数量 FilterFunc func(item interface{}) bool func (s Stream) Filter(filterFunc FilterFunc, opts ...Option) Stream { return s.Walk(func(item interface{}, pip chan<- interface{}) { if filterFunc(item) { pip <- item } }, opts...) } 使用示例: func TestInternalStream_Filter(t *testing.T) { // 保留偶数 2,4 channel := Just(1, 2, 3, 4, 5).Filter(func(item interface{}) bool { return item.(int)%2 == 0 }).channel() for item := range channel { t.Log(item) } } 遍历执行 Walk walk 英文意思是步行,这里的意思是对每个 item 都执行一次 WalkFunc 操作并将结果写入到新的 Stream 中。 这里注意一下因为内部采用了协程机制异步执行读取和写入数据所以新的 Stream 中 channel 里面的数据顺序是随机的。 // item-stream中的item元素 // pipe-item符合条件则写入pipe WalkFunc func(item interface{}, pipe chan<- interface{}) func (s Stream) Walk(fn WalkFunc, opts ...Option) Stream { option := buildOptions(opts...) if option.unlimitedWorkers { return s.walkUnLimited(fn, option) } return s.walkLimited(fn, option) } func (s Stream) walkUnLimited(fn WalkFunc, option *rxOptions) Stream { // 创建带缓冲区的channel // 默认为16,channel中元素超过16将会被阻塞 pipe := make(chan interface{}, defaultWorkers) go func() { var wg sync.WaitGroup for item := range s.source { // 需要读取s.source的所有元素 // 这里也说明了为什么channel最后写完记得完毕 // 如果不关闭可能导致协程一直阻塞导致泄漏 // 重要, 不赋值给val是个典型的并发陷阱,后面在另一个goroutine里使用了 val := item wg.Add(1) // 安全模式下执行函数 threading.GoSafe(func() { defer wg.Done() fn(item, pipe) }) } wg.Wait() close(pipe) }() // 返回新的Stream return Range(pipe) } func (s Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream { pipe := make(chan interface{}, option.workers) go func() { var wg sync.WaitGroup // 控制协程数量 pool := make(chan lang.PlaceholderType, option.workers) for item := range s.source { // 重要, 不赋值给val是个典型的并发陷阱,后面在另一个goroutine里使用了 val := item // 超过协程限制时将会被阻塞 pool <- lang.Placeholder // 这里也说明了为什么channel最后写完记得完毕 // 如果不关闭可能导致协程一直阻塞导致泄漏 wg.Add(1) // 安全模式下执行函数 threading.GoSafe(func() { defer func() { wg.Done() //执行完成后读取一次pool释放一个协程位置 <-pool }() fn(item, pipe) }) } wg.Wait() close(pipe) }() return Range(pipe) } 使用案例: 返回的顺序是随机的。 func Test_Stream_Walk(t *testing.T) { // 返回 300,100,200 Just(1, 2, 3).Walk(func(item interface{}, pip chan<- interface{}) { pip <- item.(int) * 100 }, WithWorkers(3)).ForEach(func(item interface{}) { t.Log(item) }) } 分组 Group 通过对 item 匹配放入 map 中。 KeyFunc func(item interface{}) interface{} func (s Stream) Group(fn KeyFunc) Stream { groups := make(map[interface{}][]interface{}) for item := range s.source { key := fn(item) groups[key] = append(groups[key], item) } source := make(chan interface{}) go func() { for _, group := range groups { source <- group } close(source) }() return Range(source) } 获取前 n 个元素 Head n 大于实际数据集长度的话将会返回全部元素 func (s Stream) Head(n int64) Stream { if n < 1 { panic("n must be greather than 1") } source := make(chan interface{}) go func() { for item := range s.source { n-- // n值可能大于s.source长度,需要判断是否>=0 if n >= 0 { source <- item } // let successive method go ASAP even we have more items to skip // why we don't just break the loop, because if break, // this former goroutine will block forever, which will cause goroutine leak. // n==0说明source已经写满可以进行关闭了 // 既然source已经满足条件了为什么不直接进行break跳出循环呢? // 作者提到了防止协程泄漏 // 因为每次操作最终都会产生一个新的Stream,旧的Stream永远也不会被调用了 if n == 0 { close(source) break } } // 上面的循环跳出来了说明n大于s.source实际长度 // 依旧需要显示关闭新的source if n > 0 { close(source) } }() return Range(source) } 使用示例: // 返回1,2 func TestInternalStream_Head(t *testing.T) { channel := Just(1, 2, 3, 4, 5).Head(2).channel() for item := range channel { t.Log(item) } } 获取后 n 个元素 Tail 这里很有意思,为了确保拿到最后 n 个元素使用环形切片 Ring 这个数据结构,先了解一下 Ring 的实现。 // 环形切片 type Ring struct { elements []interface{} index int lock sync.Mutex } func NewRing(n int) *Ring { if n < 1 { panic("n should be greather than 0") } return &Ring{ elements: make([]interface{}, n), } } // 添加元素 func (r *Ring) Add(v interface{}) { r.lock.Lock() defer r.lock.Unlock() // 将元素写入切片指定位置 // 这里的取余实现了循环写效果 r.elements[r.index%len(r.elements)] = v // 更新下次写入位置 r.index++ } // 获取全部元素 // 读取顺序保持与写入顺序一致 func (r *Ring) Take() []interface{} { r.lock.Lock() defer r.lock.Unlock() var size int var start int // 当出现循环写的情况时 // 开始读取位置需要通过去余实现,因为我们希望读取出来的顺序与写入顺序一致 if r.index > len(r.elements) { size = len(r.elements) // 因为出现循环写情况,当前写入位置index开始为最旧的数据 start = r.index % len(r.elements) } else { size = r.index } elements := make([]interface{}, size) for i := 0; i < size; i++ { // 取余实现环形读取,读取顺序保持与写入顺序一致 elements[i] = r.elements[(start+i)%len(r.elements)] } return elements } 总结一下环形切片的优点: 支持自动滚动更新 节省内存 环形切片能实现固定容量满的情况下旧数据不断被新数据覆盖,由于这个特性可以用于读取 channel 后 n 个元素。 func (s Stream) Tail(n int64) Stream { if n < 1 { panic("n must be greather than 1") } source := make(chan interface{}) go func() { ring := collection.NewRing(int(n)) // 读取全部元素,如果数量>n环形切片能实现新数据覆盖旧数据 // 保证获取到的一定最后n个元素 for item := range s.source { ring.Add(item) } for _, item := range ring.Take() { source <- item } close(source) }() return Range(source) } 那么为什么不直接使用 len(source) 长度的切片呢? 答案是节省内存。凡是涉及到环形类型的数据结构时都具备一个优点那就省内存,能做到按需分配资源。 使用示例: func TestInternalStream_Tail(t *testing.T) { // 4,5 channel := Just(1, 2, 3, 4, 5).Tail(2).channel() for item := range channel { t.Log(item) } // 1,2,3,4,5 channel2 := Just(1, 2, 3, 4, 5).Tail(6).channel() for item := range channel2 { t.Log(item) } } 元素转换Map 元素转换,内部由协程完成转换操作,注意输出channel并不保证按原序输出。 MapFunc func(intem interface{}) interface{} func (s Stream) Map(fn MapFunc, opts ...Option) Stream { return s.Walk(func(item interface{}, pip chan<- interface{}) { pip <- fn(item) }, opts...) } 使用示例: func TestInternalStream_Map(t *testing.T) { channel := Just(1, 2, 3, 4, 5, 2, 2, 2, 2, 2, 2).Map(func(item interface{}) interface{} { return item.(int) * 10 }).channel() for item := range channel { t.Log(item) } } 合并 Merge 实现比较简单,我考虑了很久没想到有什么场景适合这个方法。 func (s Stream) Merge() Stream { var items []interface{} for item := range s.source { items = append(items, item) } source := make(chan interface{}, 1) source <- items return Range(source) } 反转 Reverse 反转 channel 中的元素。反转算法流程是: 找到中间节点 节点两边开始两两交换 注意一下为什么获取 s.source 时用切片来接收呢? 切片会自动扩容,用数组不是更好吗? 其实这里是不能用数组的,因为不知道 Stream 写入 source 的操作往往是在协程异步写入的,每个 Stream 中的 channel 都可能在动态变化,用流水线来比喻 Stream 工作流程的确非常形象。 func (s Stream) Reverse() Stream { var items []interface{} for item := range s.source { items = append(items, item) } for i := len(items)/2 - 1; i >= 0; i-- { opp := len(items) - 1 - i items[i], items[opp] = items[opp], items[i] } return Just(items...) } 使用示例: func TestInternalStream_Reverse(t *testing.T) { channel := Just(1, 2, 3, 4, 5).Reverse().channel() for item := range channel { t.Log(item) } } 排序 Sort 内网调用 slice 官方包的排序方案,传入比较函数实现比较逻辑即可。 func (s Stream) Sort(fn LessFunc) Stream { var items []interface{} for item := range s.source { items = append(items, item) } sort.Slice(items, func(i, j int) bool { return fn(i, j) }) return Just(items...) } 使用示例: // 5,4,3,2,1 func TestInternalStream_Sort(t *testing.T) { channel := Just(1, 2, 3, 4, 5).Sort(func(a, b interface{}) bool { return a.(int) > b.(int) }).channel() for item := range channel { t.Log(item) } } 拼接 Concat func (s Stream) Concat(steams ...Stream) Stream { // 创建新的无缓冲channel source := make(chan interface{}) go func() { // 创建一个waiGroup对象 group := threading.NewRoutineGroup() // 异步从原channel读取数据 group.Run(func() { for item := range s.source { source <- item } }) // 异步读取待拼接Stream的channel数据 for _, stream := range steams { // 每个Stream开启一个协程 group.Run(func() { for item := range stream.channel() { source <- item } }) } // 阻塞等待读取完成 group.Wait() close(source) }() // 返回新的Stream return Range(source) } 汇总 API 全部匹配 AllMatch func (s Stream) AllMatch(fn PredicateFunc) bool { for item := range s.source { if !fn(item) { // 需要排空 s.source,否则前面的goroutine可能阻塞 go drain(s.source) return false } } return true } 任意匹配 AnyMatch func (s Stream) AnyMatch(fn PredicateFunc) bool { for item := range s.source { if fn(item) { // 需要排空 s.source,否则前面的goroutine可能阻塞 go drain(s.source) return true } } return false } 一个也不匹配 NoneMatch func (s Stream) NoneMatch(fn func(item interface{}) bool) bool { for item := range s.source { if fn(item) { // 需要排空 s.source,否则前面的goroutine可能阻塞 go drain(s.source) return false } } return true } 数量统计 Count func (s Stream) Count() int { var count int for range s.source { count++ } return count } 清空 Done func (s Stream) Done() { // 排空 channel,防止 goroutine 阻塞泄露 drain(s.source) } 迭代全部元素 ForAll func (s Stream) ForAll(fn ForAllFunc) { fn(s.source) } 迭代每个元素 ForEach func (s Stream) ForAll(fn ForAllFunc) { fn(s.source) } 小结 至此 Stream 组件就全部实现完了,核心逻辑是利用 channel 当做管道,数据当做水流,不断的用协程接收/写入数据到 channel 中达到异步非阻塞的效果。 回到开篇提到的问题,未动手前想要实现一个 stream 难度似乎非常大,很难想象在 go 中 300 多行的代码就能实现如此强大的组件。 实现高效的基础来源三个语言特性: channel 协程 函数式编程 参考资料 pipeline模式 切片反转算法 项目地址 https://github.com/zeromicro/go-zero https://gitee.com/kevwan/go-zero 欢迎使用 go-zero 并 star 支持我们! 微信交流群 关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

优秀的个人博客,低调大师

Mysql 流增量写入 Hdfs(二) --Storm + hdfs 的流式处理

一. 概述 上一篇我们介绍了如何将数据从 mysql 抛到 kafka,这次我们就专注于利用 storm 将数据写入到 hdfs 的过程,由于 storm 写入 hdfs 的可定制东西有些多,我们先不从 kafka 读取,而先自己定义一个 Spout 数据充当数据源,下章再进行整合。这里默认你是拥有一定的 storm 知识的基础,起码知道 Spout 和 bolt 是什么。 写入 hdfs 可以有以下的定制策略: 自定义写入文件的名字 定义写入内容格式 满足给定条件后更改写入的文件 更改写入文件时触发的 Action 本篇会先说明如何用 storm 写入 HDFS,写入过程一些 API 的描述,以及最后给定一个例子: storm 每接收到 10 个 Tuple 后就会改变 hdfs 写入文件,新文件的名字就是第几次改变。 ps:storm 版本:1.1.1 。Hadoop 版本:2.7.4 。 接下来我们首先看看 Storm 如何写入 HDFS 。 二. Storm 写入 HDFS Storm 官方有提供了相应的 API 让我们可以使用。可以通过创建 HdfsBolt 以及定义相应的规则,即可写入 HDFS 。 首先通过 maven 配置依赖以及插件。 <properties> <storm.version>1.1.1</storm.version> </properties> <dependencies> <dependency> <groupId>org.apache.storm</groupId> <artifactId>storm-core</artifactId> <version>${storm.version}</version> <!--<scope>provided</scope>--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>15.0</version> </dependency> <!--hadoop模块--> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>2.7.4</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-hdfs</artifactId> <version>2.7.4</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.storm/storm-hdfs --> <dependency> <groupId>org.apache.storm</groupId> <artifactId>storm-hdfs</artifactId> <version>1.1.1</version> <!--<scope>test</scope>--> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <executions> <execution> <goals> <goal>exec</goal> </goals> </execution> </executions> <configuration> <executable>java</executable> <includeProjectDependencies>true</includeProjectDependencies> <includePluginDependencies>false</includePluginDependencies> <classpathScope>compile</classpathScope> <mainClass>com.learningstorm.kafka.KafkaTopology</mainClass> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>1.7</version> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass></mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> 这里要提一下,如果要打包部署到集群上的话,打包的插件需要使用 maven-shade-plugin 这个插件,然后使用 maven Lifecycle 中的 package 打包。而不是用 Maven-assembly-plugin 插件进行打包。 因为使用 Maven-assembly-plugin 的时候,会将所有依赖的包unpack,然后在pack,这样就会出现,同样的文件被覆盖的情况。发布到集群上的时候就会报 No FileSystem for scheme: hdfs 的错 。 然后是使用 HdfsBolt 写入 Hdfs。这里来看看官方文档中的例子吧。 // 使用 "|" 来替代 ",",来进行字符分割 RecordFormat format = new DelimitedRecordFormat() .withFieldDelimiter("|"); // 每输入 1k 后将内容同步到 Hdfs 中 SyncPolicy syncPolicy = new CountSyncPolicy(1000); // 当文件大小达到 5MB ,转换写入文件,即写入到一个新的文件中 FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(5.0f, Units.MB); //当转换写入文件时,生成新文件的名字并使用 FileNameFormat fileNameFormat = new DefaultFileNameFormat() .withPath("/foo/"); HdfsBolt bolt = new HdfsBolt() .withFsUrl("hdfs://localhost:9000") .withFileNameFormat(fileNameFormat) .withRecordFormat(format) .withRotationPolicy(rotationPolicy) .withSyncPolicy(syncPolicy); //生成该 bolt topologyBuilder.setBolt("hdfsBolt", bolt, 5).globalGrouping("randomStrSpout"); 到这里就结束了。可以将 HdfsBolt 当作一个 Storm 中特殊一些的 bolt 即可。这个 bolt 的作用即使根据接收信息写入 Hdfs。 而在新建 HdfsBolt 中,Storm 为我们提供了相当强的灵活性,我们可以定义一些策略,比如当达成某个条件的时候转换写入文件,新写入文件的名字,写入时候的分隔符等等。 如果选择使用的话,Storm 有提供部分接口供我们使用,但如果我们觉得不够丰富也可以自定义相应的类。下面我们看看如何控制这些策略吧。 RecordFormat 这是一个接口,允许你自由定义接收到内容的格式。 public interface RecordFormat extends Serializable { byte[] format(Tuple tuple); } Storm 提供了 DelimitedRecordFormat ,使用方法在上面已经有了。这个类默认的分割符是逗号",",而你可以通过 withFieldDelimiter 方法改变分隔符。如果你的初始分隔符不是逗号的话,那么也可以重写写一个类实现 RecordFormat 接口即可。 FileNameFormat 同样是一个接口。 public interface FileNameFormat extends Serializable { void prepare(Map conf, TopologyContext topologyContext); String getName(long rotation, long timeStamp); String getPath(); } Storm 所提供的默认的是 org.apache.storm.hdfs.format.DefaultFileNameFormat 。默认人使用的转换文件名有点长,格式是这样的: {prefix}{componentId}-{taskId}-{rotationNum}-{timestamp}{extension} 例如: MyBolt-5-7-1390579837830.txt 默认情况下,前缀是空的,扩展标识是".txt"。 SyncPolicy 同步策略允许你将 buffered data 缓冲到 Hdfs 文件中(从而client可以读取数据),通过实现org.apache.storm.hdfs.sync.SyncPolicy 接口: public interface SyncPolicy extends Serializable { boolean mark(Tuple tuple, long offset); void reset(); } FileRotationPolicy 这个接口允许你控制什么情况下转换写入文件。 public interface FileRotationPolicy extends Serializable { boolean mark(Tuple tuple, long offset); void reset(); } Storm 有提供三个实现该接口的类: 最简单的就是不进行转换的org.apache.storm.hdfs.bolt.rotation.NoRotationPolicy ,就是什么也不干。 通过文件大小触发转换的 org.apache.storm.hdfs.bolt.rotation.FileSizeRotationPolicy。 通过时间条件来触发转换的 org.apache.storm.hdfs.bolt.rotation.TimedRotationPolicy。 如果有更加复杂的需求也可以自己定义。 RotationAction 这个主要是提供一个或多个 hook ,可加可不加。主要是在触发写入文件转换的时候会启动。 public interface RotationAction extends Serializable { void execute(FileSystem fileSystem, Path filePath) throws IOException; } 三.实现一个例子 了解了上面的情况后,我们会实现一个例子,根据写入记录的多少来控制写入转换(改变写入的文件),并且转换后文件的名字表示当前是第几次转换。 首先来看看 HdfsBolt 的内容: RecordFormat format = new DelimitedRecordFormat().withFieldDelimiter(" "); // sync the filesystem after every 1k tuples SyncPolicy syncPolicy = new CountSyncPolicy(1000); // FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(1.0f, FileSizeRotationPolicy.Units.KB); /** rotate file with Date,every month create a new file * format:yyyymm.txt */ FileRotationPolicy rotationPolicy = new CountStrRotationPolicy(); FileNameFormat fileNameFormat = new TimesFileNameFormat().withPath("/test/"); RotationAction action = new NewFileAction(); HdfsBolt bolt = new HdfsBolt() .withFsUrl("hdfs://127.0.0.1:9000") .withFileNameFormat(fileNameFormat) .withRecordFormat(format) .withRotationPolicy(rotationPolicy) .withSyncPolicy(syncPolicy) .addRotationAction(action); 然后分别来看各个策略的类。 FileRotationPolicy import org.apache.storm.hdfs.bolt.rotation.FileRotationPolicy; import org.apache.storm.tuple.Tuple; import java.text.SimpleDateFormat; import java.util.Date; /** * 计数以改变Hdfs写入文件的位置,当写入10次的时候,则更改写入文件,更改名字取决于 “TimesFileNameFormat” * 这个类是线程安全 */ public class CountStrRotationPolicy implements FileRotationPolicy { private SimpleDateFormat df = new SimpleDateFormat("yyyyMM"); private String date = null; private int count = 0; public CountStrRotationPolicy(){ this.date = df.format(new Date()); // this.date = df.format(new Date()); } /** * Called for every tuple the HdfsBolt executes. * * @param tuple The tuple executed. * @param offset current offset of file being written * @return true if a file rotation should be performed */ @Override public boolean mark(Tuple tuple, long offset) { count ++; if(count == 10) { System.out.print("num :" +count + " "); count = 0; return true; } else { return false; } } /** * Called after the HdfsBolt rotates a file. */ @Override public void reset() { } @Override public FileRotationPolicy copy() { return new CountStrRotationPolicy(); } } FileNameFormat import org.apache.storm.hdfs.bolt.format.FileNameFormat; import org.apache.storm.task.TopologyContext; import java.util.Map; /** * 决定重新写入文件时候的名字 * 这里会返回是第几次转换写入文件,将这个第几次做为文件名 */ public class TimesFileNameFormat implements FileNameFormat { //默认路径 private String path = "/storm"; //默认后缀 private String extension = ".txt"; private Long times = new Long(0); public TimesFileNameFormat withPath(String path){ this.path = path; return this; } @Override public void prepare(Map conf, TopologyContext topologyContext) { } @Override public String getName(long rotation, long timeStamp) { times ++ ; //返回文件名,文件名为更换写入文件次数 return times.toString() + this.extension; } public String getPath(){ return this.path; } } RotationAction import org.apache.hadoop.fs.FileContext; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.storm.hdfs.common.rotation.RotationAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; /** 当转换写入文件时候调用的 hook ,这里仅写入日志。 */ public class NewFileAction implements RotationAction { private static final Logger LOG = LoggerFactory.getLogger(NewFileAction.class); @Override public void execute(FileSystem fileSystem, Path filePath) throws IOException { LOG.info("Hdfs change the written file!!"); return; } } OK,这样就大功告成了。通过上面的代码,每接收到 10 个 Tuple 后就会转换写入文件,新文件的名字就是第几次转换。 完整代码包括一个随机生成字符串的 Spout ,可以到我的 github 上查看。 StormHdfsDemo:https://github.com/shezhiming/StormHdfsDemo 更多干货,欢迎关注公众号,哈尔的数据城堡。

优秀的个人博客,低调大师

LeaferJS 1.0 重磅发布:强悍的前端 Canvas 渲染引擎

前言​ 大家好,我是 LeaferJS 的创始人万超。 盛夏的绿叶在雨后阳光的照耀下熠熠生辉,正如 Leafer 的寓意:看见希望,不断成长。 在此,我很高兴的向大家宣布,经过我们 3 年多开发的 LeaferJS 开源引擎终于打磨成熟,迎来了正式版的发布~ 🎉🎉🎉 这距离我们上次公测版的发布,已经过去了整整一年。 与用户相互成就​ 在过去的一年里,无数的用户需求和反馈如潮水般涌来,不断地滋润和雕琢着这款产品,使其功能更加丰富,且更贴合实际使用场景,同时我们的文档也变得更加通俗易懂。即使期间产品还不完善,我们依然无法阻挡用户的热情!通过口碑传播的用户源源不断的涌来~ 感谢每一位撰写文章、教程、传播、提 Bug、建议、开发产品、Demo、插件、慷慨赞助的用户~ 随着正式版的发布,更多的企业可以将 LeaferJS 用于生产环境,其周边生态也将迎来蓬勃发展。 由浅入深带你了解 LeaferJS​ 熟悉我们的朋友都知道,我们一直在朝着一个极具吸引力的伟大愿景迈进。我们的引擎能在短短的 1.5 秒内创建 100 万个可交互矩形,仅占用 350MB 内存。在继续保持这一优势的同时,正式版带来了众多实用的功能,拓宽了使用场景。 下面让我由浅入深,从前端开发的视角,带你看看 LeaferJS 可以如何应用在你的工作中。 首先,让我们通过代码看看 LeaferJS 是如何工作的: 接下来,我们会从以下三个入门场景开始说起: 最后,再聊一聊压轴的高级场景以及我们的愿景。 高效绘图​ 相信每家公司的产品都有生成长图、产品海报进行分享的需求,尤其是在小程序中。 我们提供了易于上手的自动布局功能(类似 Flex 但更简洁),以及按中心点绘制的功能。这将为绘制海报带来一种全新的开发体验,应对频繁变化的需求,无需再手动测量文本进行布局。此外,通过简单易控的渐变、内外阴影、遮罩、擦除等功能,能够轻松实现各种专业的设计效果。 我们还提供了一支神奇的 Pen,可以像绘画一样,快速画出不同样式的路径和元素组合。 import { Leafer, Pen } from 'leafer-ui' const leafer = new Leafer({ view: window }) const pen = new Pen() pen.setStyle({ fill: { type: 'radial', stops: ['#FF4B4B', '#FEB027'] } }) pen.roundRect(0, 0, 100, 100, 30) pen.setStyle({ y: -5, fill: 'white' }) pen.moveTo(40, 30).bezierCurveTo(70, 30, 90, 60, 63, 80).quadraticCurveTo(50, 88, 40, 80).bezierCurveTo(10, 60, 50, 40, 40, 30) leafer.add(pen)此外,你还可以在我们官网的设计工具中提前编辑内容,将其导出为 JSON 文件,随后调整变量并导入,即可自动生成图片。更重要的是,我们支持在小程序环境中生成海报图片,并且支持多倍图导出。 UI 交互​ 组态、流程图​ 我们提供了丰富的箭头样式和元素状态,结合用户提供的连线插件,你可以快速的搭建一个组态、流程图软件。 UI 交互界面​ 在 Canvas 环境中实现一套 UI 交互界面一直是业内的挑战,我们用极少的代码初步实现了这一目标,并提供了丰富的跨平台交互事件和手势,以及类似 CSS 的交互状态样式和光标样式。 结合自动布局的功能,使你能够快速在组态、游戏开发、数据可视化、富媒体和创意设计等场景中集成高性能的 UI 交互界面功能。未来可以朝着更高效的可视化编程方向发展。 以下是我们社区的用户正在开发的 element-plus-leafer UI 组件: 小游戏​ 我们有丰富的绘图、交互功能,再结合你喜欢的动画库、物理引擎,可以将 LeaferJS 用来开发一些好玩的小游戏。 以下是我们社区的用户正在开发的小游戏合集: 图形图像编辑​ 图片裁剪​ 相信每家公司的产品都有裁剪头像和图片的需求。 我们提供了完善的图形编辑功能,并为图片裁剪场景准备了贴心的配置,例如裁剪遮罩、手机端手指缩放和旋转,让你可以通过最简单的代码搭建起一个属于自己的图片裁剪工具。 图片编辑器​ 相信你用过各种各样的图片编辑器,自己开发一套绝非易事,而我们让每个人都可以上手尝试。 我们为图片编辑器的开发,提供了丰富的功能和 自定义编辑器样式配置, 可移动、缩放、旋转、倾斜,支持多选、框选、打组、 双击进组,锁定、层级。支持自定义编辑工具和内部编辑器。 我们还与第三方开发者携手提供了开箱即用的插件,如标尺、视窗控制、滚动条、文本编辑,对齐吸附、SVG 导出插件也即将开发,这些插件可以节省你大量的开发工作。 短视频​ 如今已是短视频横行的时代,可以通过开源的 canvas 转视频库,将 LeaferJS 的动画、编辑内容导出视频,帮助企业快速批量生成短视频。 深入了解 LeaferJS​ 相信通过以上三个入门场景的了解,你已经对 LeaferJS 有了初步认识,但我们能做的事情绝对不仅仅于此,接下来我将带你了解一些高级场景,以及我们的愿景。 在线设计​ 如果你希望打造一款专业的在线设计软件,如矢量设计软件、 UI 设计软件、白板软件、流程图设计软件,低代码编辑软件、矢量雕刻软件、CAD 建筑设计软件、EDA 电路设计软件、视频剪辑软件等, 我们可以帮助你节省这 3 年的创业时间。 我们的底层正是为开发专业级的图形图像编辑软件而架构的(因为它能创造出一切的场景),提供了开箱即用的功能和优秀的扩展能力,你可以站在 LeaferJS 的肩膀上前行,快速打造出可以改变人们生活方式的数字产品。 以下是我们官方收录的部分产品,期待未来你的产品也能出现在这里。 大型场景​ 以下是我们官网列举的一些大型应用场景(远不局限于此),可以作为你启动新产品的参考。 逐步开放​ 我们通过核心包+场景插件的设计,来逐步满足不同场景的需求,每一个场景成熟后会集成为一个场景包,方便用户快速安装使用。第三方开发者也可以参与补充场景。 我们现阶段仍主要专注于绘图、编辑器场景(核心场景),之后会开始训练 AI 绘图和提升性能。 使命与愿景​ 我们致力于通过 LeaferJS 实现一套简洁、开放、现代化的 UI 绘图语言标准,便于 AI 理解,人类可视化使用,并为数字化产品开发提供跨平台、轻量化、高性能的运行时。 我们希望不同的软件之间能够沟通、协作、共享绘图数据与数字界面,通过不断革新的图形技术和配套支持, 吸引更多的开发者加入使用,建立起一个开放的生态环境,沟通有无,以推动行业的快速发展,并诞生出更多有创意的技术和产品,尤其是 AI + 可视化编辑方向的产品。 我们的性能优化目标是想实现流畅操作 百万级静态元素 和 十万级动态游戏元素( GPU 加速)。 热爱与勇气,让梦想成为现实​ 我们不仅在用心创造一款有意义的开源产品(降低创业者的研发成本,推进技术的进步,改变未来产品的开发方式),更是在践行传播一种人人所向往的、有人文关怀的卓越文化。 创业、创新的道路是艰难和孤独的,正是这样的文化和使命感,帮助我们度过了漫长的 3 年时光,日复一日的精进,并克服了重重难关,不断的凝聚起了更多的人,一起朝着愿景前进。 随着正式版的发布,我们找到了愿意在财务上持续帮助我们的人(非投资),这让我们可以心无旁骛的持续推动 LeaferJS 这个飞轮,共同打造我们喜爱的开源社区,设计可加速发展的上层应用商业模式,让 LeaferJS 能够走向世界,惠及到更多的人。 恭喜 🎉🎉🎉​ 至此,你已完成了对 LeaferJS 全貌的了解,你又多了一个实用的提效工具,而我们也多了一位熟悉的朋友,欢迎访问我们的官网,还有很多好用的功能和有趣的故事等待你去探索发现。 www.leaferjs.com

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册