go语言以优异的并发特性而闻名,刚好手上有个小项目比较适合。

项目背景:

公司播控平台的数据存储包括MySQL和ElasticSearch(ES)两个部分,编辑、运营的数据首先保存在MySQL中,为了实现模糊搜索和产品关联推荐,特别增加了ES,ES中保存的是节目集的基本信息。

本项目是为了防止实时同步数据出现问题或者系统重新初始化时的全量数据同步而做的。项目主要是从MySQL读取所有的节目集数据写入到ES中。

项目特点:

因为节目集数量较大,不能一次性的读入内存,因此每次读出一部分记录写入ES。ORM使用的是beego。为了提高性能使用了协程,其中读MySQL的部分最大开启20个协程,ES写入部分开启了15个协程(因为ES分片设置的问题,5个协程和15个协程性能映像不大)。

项目主要包括三个文件:

1、PrdES_v3.go,项目的入口,负责协调MySQL读取和ES写入。

package main

import (
"./db"
"./es"
//"encoding/json"
"fmt"
"reflect"
"time"
) type PrdES struct {
DB *prd.Mysql
ES *es.Elastic
} // func (this *PrdES) Handle(result []*prd.Series) {
// // for _, value := range result {
// // this.DB.FormatData(value)
// // //json, _ := json.Marshal(value)
// // //fmt.Println(string(json))
// // }
// //写入ES,以多线程的方式执行,最多保持5个线程
// this.ES.DoBulk(result)
// }
func (this *PrdES) Run() {
count :=
offset :=
maxCount :=
//create channel
chs := make([]chan []*prd.Series, maxCount)
selectCase := make([]reflect.SelectCase, maxCount)
for i := ; i < maxCount; i++ {
offset = count * i
fmt.Println("offset:", offset)
//init channel
chs[i] = make(chan []*prd.Series)
//set select case
selectCase[i].Dir = reflect.SelectRecv
selectCase[i].Chan = reflect.ValueOf(chs[i])
//运行
go this.DB.GetData(offset, count, chs[i])
}
var result []*prd.Series
for {
//wait data return
chosen, recv, ok := reflect.Select(selectCase)
if ok {
fmt.Println("channel id:", chosen)
result = recv.Interface().([]*prd.Series) //读取数据从mysql
go this.DB.GetData(offset, count, chs[chosen]) //写入ES,以多线程的方式执行,最多保持15个线程
this.ES.DoBulk(result)
//update offset
offset = offset + len(result)
//判断是否到达数据尾部,最后一次查询
if len(result) < count {
fmt.Println("read end of DB")
//等所有的任务执行完毕
this.ES.Over()
fmt.Println("MySQL Total:", this.DB.GetTotal(), ",Elastic Total:", this.ES.GetTotal())
return }
}
} } func main() {
s := time.Now()
fmt.Println("start")
pe := new(PrdES) pe.DB = prd.NewDB()
pe.ES = es.NewES()
//fmt.Println("mysql info:")
//fmt.Println("ES info:")
pe.Run() fmt.Println("time out:", time.Since(s).Seconds(), "(s)")
fmt.Println("Over!") }

在run函数里可以看到使用了reflect.SelectCase,使用reflect.SelectCase的原因是读MySQL数据是多个协程,不可预计哪个会首先返回,selectCase是任何一个处理完毕reflect.Select函数就会返回,MySQL读取的数据放在channel中宕Select函数返回时chosen, recv, ok := reflect.Select(selectCase)判断ok是否未true            chosen代表的是协程id通过result = recv.Interface().([]*prd.Series)获得返回的数据,因为MySQL读取的数据是对象的结果集,因次使用recv.Interface函数,如果是简单类型可以使用recv.recvInt(),recv.recvString()等函数直接获取channel返回数据。 

这里通过counter控制协程的数量,也可以通过channel,用select的方式控制协程的数量,之所以用counter计数器的方式控制协程数量是我想知道同时有多少协程在运行。

注:此处channel可以不用创建数组形式,channel带回来的数据也没有顺序问题。

2、es.go,负责写入ES和es的写入协程调度

package es

import (
"../db"
//"encoding/json"
"fmt"
elastigo "github.com/mattbaird/elastigo/lib"
//elastigo "github.com/Uncodin/elastigo/lib"
//"github.com/Uncodin/elastigo/core"
"time"
//"bytes"
"flag"
"sync"
//"github.com/fatih/structs"
) var (
//开发测试库
//host = flag.String("host", "192.168.1.236", "Elasticsearch Host")
//C平台线上
host = flag.String("host", "192.168.100.23", "Elasticsearch Host")
port = flag.String("port", "", "Elasticsearch port")
) //indexor := core.NewBulkIndexorErrors(10, 60)
// func init() {
// //connect to elasticsearch
// fmt.Println("connecting es")
// //api.Domain = *host //"192.168.1.236"
// //api.Port = "9300" // }
//save thread count
var counter int type Elastic struct {
//Seq int64
c *elastigo.Conn
lock *sync.Mutex
lockTotal *sync.Mutex
wg *sync.WaitGroup
total int64
} func (this *Elastic) Conn() {
this.c = elastigo.NewConn()
this.c.Domain = *host
this.c.Port = *port
//NewClient(fmt.Sprintf("%s:%d", *host, *port))
}
func (this *Elastic) CreateLock() {
this.lock = &sync.Mutex{}
this.lockTotal = &sync.Mutex{}
this.wg = &sync.WaitGroup{}
counter =
this.total =
}
func NewES() (es *Elastic) {
//connect elastic
es = new(Elastic)
es.Conn()
//create lock
es.CreateLock()
return es
}
func (this *Elastic) DoBulk(series []*prd.Series) {
for true {
this.lock.Lock()
if counter < {
//跳出,执行任务
break
} else {
this.lock.Unlock()
//等待100毫秒
//fmt.Println("wait counter less than 25, counter:", counter)
time.Sleep(1e8)
}
}
this.lock.Unlock()
//执行任务
go this.bulk(series, this.lock)
}
func (this *Elastic) Over() {
this.wg.Wait()
/*for {
this.lock.Lock()
if counter <= 0 {
this.lock.Unlock()
break
}
this.lock.Unlock()
}
*/
} func (this *Elastic) GetTotal() (t int64) {
this.lockTotal.Lock()
t = this.total
this.lockTotal.Unlock()
return t
}
func (this *Elastic) bulk(series []*prd.Series, lock *sync.Mutex) (succCount int64) {
//增加计数器
this.wg.Add()
//减少计数器
defer this.wg.Done() //加计数器
lock.Lock()
counter++
fmt.Println("add task, coutner:", counter)
lock.Unlock() //设置初始成功写入的数量
succCount = for _, value := range series {
//json, _ := json.Marshal(value)
//fmt.Println(string(json))
if value.ServiceGroup != nil {
fmt.Println("series code:", value.Code, ",ServiceGroup:", value.ServiceGroup) resp, err := this.c.Index("guttv", "series", value.Code, nil, *value) if err != nil {
panic(err)
} else {
//fmt.Println(value.Code + " write to ES succsessful!")
fmt.Println(resp)
succCount++
}
} else {
fmt.Println("series code:", value.Code, "service group is null")
}
} //计数器减一
lock.Lock()
counter--
fmt.Println("reduce task, coutner:", counter, ",success count:", succCount)
lock.Unlock() this.lockTotal.Lock()
this.total = this.total + succCount
this.lockTotal.Unlock()
return succCount
}

在es.go里有两把锁lock和lockTotal,前者是针对counter变量,记录es正在写入的协程数量的;后者为记录总共写入多少条记录到es而增加的。

这里必须要提到的是Over函数,初步使用协程的容易忽略。golang的原则是当main函数运行结束后,所有正在运行的协程都会终止,因袭在MySQL读取数据完毕必须调用Over函数,等待所有的协程结束。这里使用sync.waiGrooup。每次协程启动执行下面两个语句:

//增加计数器
this.wg.Add(1)
//减少计数器,函数结束时自动执行
defer this.wg.Done()
Over函数中调用wg.Wait()等待计数器为0时返回,否则就一直阻塞。当然读者也可以看到通过检查counter是否小于等于0也可以判断协程是否都结束(Over函数被注释的部分),显然使用waitGroup更优雅和高效。

 

3、db.go,负责MySQL数据的读取

package prd

import (
"fmt"
"github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql" // import your used driver
"strings"
"sync"
"time"
) func init() { orm.RegisterDataBase("default", "mysql", "@tcp(192.168.100.3306)/guttv_vod?charset=utf8", ) orm.RegisterModelWithPrefix("t_", new(Series), new(Product), new(ServiceGroup))
orm.RunSyncdb("default", false, false)
} type Mysql struct {
sql string
total int64
lock *sync.Mutex
} func (this *Mysql) New() {
//this.sql = "SELECT s.*, p.code ProductCode, p.name pName FROM guttv_vod.t_series s inner join guttv_vod.t_product p on p.itemcode=s.code and p.isdelete=0 limit ?,?"
this.sql = "SELECT s.*, p.code ProductCode, p.name pName FROM guttv_vod.t_series s , guttv_vod.t_product p where p.itemcode=s.code and p.isdelete=0 limit ?,?"
this.total =
this.lock = &sync.Mutex{}
}
func NewDB() (db *Mysql) {
db = new(Mysql)
db.New()
return db
}
func (this *Mysql) GetTotal() (t int64) {
t =
this.lock.Lock()
t = this.total
this.lock.Unlock()
return t
}
func (this *Mysql) toTime(toBeCharge string) int64 {
timeLayout := "2006-01-02 15:04:05"
loc, _ := time.LoadLocation("Local")
theTime, _ := time.ParseInLocation(timeLayout, toBeCharge, loc)
sr := theTime.Unix()
if sr < {
sr =
}
return sr
}
func (this *Mysql) getSGCode(seriesCode string) (result []string, num int64) {
sql := "select distinct ref.servicegroupcode code from t_servicegroup_reference_category ref "
sql = sql + "left join t_category_product cp on cp.categorycode=ref.categorycode "
sql = sql + "left join t_package pkg on pkg.code = cp.assetcode "
sql = sql + "left join t_package_product pp on pp.parentcode=pkg.code "
sql = sql + "left join t_product prd on prd.code = pp.assetcode "
sql = sql + "where prd.itemcode=?"
o := orm.NewOrm()
var sg []*ServiceGroup
num, err := o.Raw(sql, seriesCode).QueryRows(&sg) if err == nil {
//fmt.Println(num)
for _, value := range sg {
//fmt.Println(value.Code)
result = append(result, value.Code)
} } else {
fmt.Println(err)
}
//fmt.Println(result)
return result, num
} func (this *Mysql) formatData(value *Series) {
//设置业务分组数据
sg, _ := this.getSGCode(value.Code)
//fmt.Println(sg)
value.ServiceGroup = []string{}
value.ServiceGroup = sg[:]
//更改OnlineTime为整数
value.OnlineTimeInt = this.toTime(value.OnlineTime)
//分解地区
value.OriginalCountryArr = strings.Split(value.OriginalCountry, "|")
//分解二级分类
value.ProgramType2Arr = strings.Split(value.ProgramType2, "|")
//写入记录内容
value.Description = strings.Replace(value.Description, "\n", "", -)
}
func (this *Mysql) GetData(offset int, size int, ch chan []*Series) {
var result []*Series
o := orm.NewOrm()
num, err := o.Raw(this.sql, offset, size).
QueryRows(&result)
if err != nil {
fmt.Println("read DB err")
panic(err)
//return //err, nil
}
for _, value := range result {
this.formatData(value)
//json, _ := json.Marshal(value)
//fmt.Println(string(json))
//fmt.Println(value.ServiceGroup)
}
this.lock.Lock()
this.total += num
this.lock.Unlock() fmt.Println("read count :", num) //, "Total:", Total)
//return nil, result
ch <- result
}

从项目上看。go语言开发还是比较简洁的,多协程实现也相对容易,但要求开发者必须对概念非常清晰,像select和selectCase理解和defer的理解要很到位,个人层经做过多年的C/C++程序员,从经验上看,C/C++的经验(多线程的理解)对运用go语言还是很有帮助的。

golang的多协程实践的更多相关文章

  1. golang中最大协程数的限制(线程)

    golang中最大协程数的限制 golang中有最大协程数的限制吗?如果有的话,是通过什么参数控制呢?还是通过每个协程占用的资源计算? 通过channel控制协程数的就忽略吧. 以我的理解,计算机资源 ...

  2. Golang 入门 : goroutine(协程)

    在操作系统中,执行体是个抽象的概念.与之对应的实体有进程.线程以及协程(coroutine).协程也叫轻量级的线程,与传统的进程和线程相比,协程的最大特点是 "轻"!可以轻松创建上 ...

  3. Golang的goroutine协程和channel通道

    一:简介 因为并发程序要考虑很多的细节,以保证对共享变量的正确访问,使得并发编程在很多情况下变得很复杂.但是Go语言在开发并发时,是比较简洁的.它通过channel来传递数据.数据竞争这个问题在gol ...

  4. Java协程实践指南(一)

    一. 协程产生的背景 说起协程,大多数人的第一印象可能就是GoLang,这也是Go语言非常吸引人的地方之一,它内建的并发支持.Go语言并发体系的理论是C.A.R Hoare在1978年提出的CSP(C ...

  5. golang:Channel协程间通信

    channel是Go语言中的一个核心数据类型,channel是一个数据类型,主要用来解决协程的同步问题以及协程之间数据共享(数据传递)的问题.在并发核心单元通过它就可以发送或者接收数据进行通讯,这在一 ...

  6. golang 并发之协程及通道

    一.概述 在golang中,每个并发执行单元称为goroutine,当程序启动时,main函数在一个单独的goroutine中运行,(main goroutine).新的goroutine会用go语句 ...

  7. Golang等待一组协程结束

    1. 利用waitgroup import ( "log" "sync" "sync/atomic" "time" ) ...

  8. golang中goroutine协程调度器设计策略

    goroutine与线程 /* goroutine与线程1. 可增长的栈os线程一般都有固定的栈内存,通常为2MB,一个goroutine的在其声明周期开始时只有很小的栈(2KB),goroutine ...

  9. 写了一年golang,来聊聊进程、线程与协程

    本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star. 进程 在早期的单任务计算机中,用户一次只能提交一个作业,独享系统的全部资源,同时也只能干一件事 ...

随机推荐

  1. winform 查看远程服务器上的文件

    解决方案: 1. 在目标服务器上发布webservice,实现文件下载的方法. using System; using System.Collections.Generic; using System ...

  2. OkHttp+Stetho+Chrome调试android网络部分(原创)

    android网络调试一直是一个比较麻烦的部分,因为在不同序列的请求中,返回的数据会有不同的变化,如果能像web开发一样使用调试功能查看页面的访问数据该是多么美好的事情! 很幸运的是,现在Androi ...

  3. Memcached集群:Magent缓存代理使用

    小结: 先启动memcached 然后启动magent memcached -d -p 11211 -u memcached -m 64 -c 5120 memcached -d -p 11212 - ...

  4. MySQL集群系列1:2台机器搭建双主集群

    先配置静态IP 2台机器mysql密码一样,最好在同一局域网内,最好在mysql刚安装时就配置好,后面有数据了不好同步. 本文实现了2台机器mysql数据同步成功: 配置my.cnf 先关闭防火墙 s ...

  5. jmeter模拟对网站做压力测试

    一般的网站,在进入业务功能前先需登录,然后才能访问业务功能.基本框架如下 详细步骤: 1 .用badboy录制登录,访问随意一个网址. 2.用jmeter打开,一会自己写的时候可以参考里面的参数名称或 ...

  6. 第二百一十四节,jQuery EasyUI,Calendar(日历)组件

    jQuery EasyUI,Calendar(日历)组件 学习要点: 1.加载方式 2.属性列表 3.事件列表 4.方法列表 本节课重点了解 EasyUI 中 Canlendar(日历)组件的使用方法 ...

  7. 嵌入式驱动开发之phy---fine Mac与Phy组成原理的简单分析

    关键字rj45.pci-e 1. general 下图是网口结构简图.网口由CPU.MAC和PHY三部分组成.DMA控制器通常属于CPU的一部分,用虚线放在这里是为了表示DMA控制器可能会参与到网口数 ...

  8. IntelliJ Idea Hide excluded folders 隐藏或显示你需要的文件夹

    IntelliJ Idea,以前用idea时,经常maven编译就出现了exclude下的文件夹通常是target,如何隐藏自己不想看见的文件夹,或显示偶尔会用到的文件夹 点击齿轮右下小标 选中文件夹 ...

  9. Microsoft Visual C++ 2005 Redistributable---win下安装软件“嘭”的一声报错!

    今天下了个MindManager,正准备安装结果出现了如题的错误提示!!! 于是百度/google一下,在权威的微软官网下找到了答案,他妈的,看了之后表示很无奈 If the non unicode ...

  10. 无法打开输入文件“optimized.lib” 编译osgEarth2.8+VS2013+CMake3.4.0在Release版本的问题

    1>LINK : fatal error LNK1181: 无法打开输入文件“optimized.lib” 可以到http://forum.osgearth.org搜索相关帖子,gwaldron ...