大文件Excel数据批量导入Mysql

368 阅读2分钟

以前在公司的时候,做过这样一个需求,用户导入一个Excel文件。excel文件中是一些员工的个人信息。通过程序获取到excel中的数据,并且经过一系列的数据的规则处理和计算,存入到数据库中。这个功能之前使用的是PHP实现的。使用PHP实现8000条数据数据就会耗时1-2分钟。今天准备使用Go再实现一遍。go+channel+groutine实现只在几秒之内。

技术点

  • golang
  • goroutine
  • channel
  • waitGroup

实现思路

  1. 连接数据库
  2. 打开excel文件,获取每一行的数据。 文件的首行(标题行)过滤。
  3. 创建channel,存每一个用户的信息
  4. 创建多个Goroutine, 批量插入数据
  5. 使用waitGroup控制并发, 要不然同一条数据可能被插入好多次

代码实现


package main

import (
   "database/sql"
   "day05/model"
   "day05/utils"
   "github.com/golang-module/carbon/v2"
   "github.com/xuri/excelize/v2"
   "log"
   "strconv"
   "sync"
)

var (
   wg       sync.WaitGroup
   ChanUser chan *model.User
)

func main() {

   // 创建 channel
   ChanUser = make(chan *model.User, 100)

   // 打开Excel文件 并且读取数据
   xlsx := OpenExcel("users.xlsx")
   ReadExcel(xlsx, ChanUser)

   // 打开数据库连接
   utils.MysqlConnection()

   // 准备 sql 语句
   stmt := PrepareSql()

   // 启动多个 goroutine 来处理大数据量的导入问题
   for i := 0; i < 10; i++ {
      go InsertDB(stmt, ChanUser)
   }

   wg.Wait()

}

// OpenExcel 打开Excel文件
func OpenExcel(url string) *excelize.File {
   xlsx, err := excelize.OpenFile(url)
   if err != nil {
      log.Fatal(err)
      return nil
   }
   return xlsx
}

// ReadExcel 读取Excel文件 读取到一行就往channel里面写
func ReadExcel(xlsx *excelize.File, ChanUser chan *model.User) {
   // 获取 Excel 第一个 sheet 的所有行
   rows, err := xlsx.GetRows("Sheet1")
   if err != nil {
      log.Fatal(err)
      return
   }

   // 遍历每一行
   for k, row := range rows {
      // 跳过第一行
      if k == 0 {
         continue
      }

      height, _ := strconv.ParseFloat(row[4], 64)
      age, _ := strconv.ParseInt(row[5], 10, 64)

      // 将每一行的数据写入到 channel 中
      wg.Add(1)
      ChanUser <- &model.User{
         Name:         row[0],
         Email:        row[1],
         Password:     row[2],
         Nickname:     row[3],
         Height:       height,
         Age:          age,
         MemberNumber: row[6],
         Username:     row[7],
         Phone:        row[8],
         Balance:      row[9],
         Birthday:     carbon.Parse(row[6]).ToStdTime(),  // 返回的是carbon的示例对象
         DeletedAt:    carbon.Parse(row[11]).ToStdTime(), // 返回的是carbon的示例对象
      }

   }
}

// PrepareSql 准备 sql 语句
func PrepareSql() *sql.Stmt {
   // 要导入的字段
   filed := []string{
      "name",
      "email",
      "password",
      "nickname",
      "height",
      "age",
      "member_number",
      "username",
      "phone",
      "balance",
      "birthday",
      "deleted_at",
   }

   // 准备 SQL 语句
   stmt, _ := utils.CreateInsertSql(filed)
   return stmt
}

// InsertDB 插入数据库
func InsertDB(stmt *sql.Stmt, ChanUser chan *model.User) {
   for user := range ChanUser {
      if _, err := stmt.Exec(user.Name, user.Email, user.Password, user.Nickname, user.Height, user.Age, user.MemberNumber, user.Username, user.Phone, user.Balance, user.Birthday, user.DeletedAt); err != nil {
         log.Fatal(err)
      }
      wg.Done()
   }
}

使用过程中产生的问题

  1. for循环中开启协程 协程过多导致程序崩掉
  2. 插入数据的时候mysql的时候会出现在同一时刻只能使用一个套接字地址进行连接的异常报错。也就是在并发环境下,多个线程或协程同时使用相同的套接字地址进行连接。解决的方案就是一次如果报错的话进行连接重试。

总结

  1. 连接数据库的使用
  2. 读取excel中的数据
  3. goroutine 和 channel 的使用。
  4. waitGroup的使用 不使用的话就会产生并发问题,导致产生脏数据。
  5. carbon 时间工具的使用 从excel中读取出来是字符串。 存入数据库要存成time.Time类型的数据