开发中常见的批量操作

70 阅读2分钟

批量插入的sql

在实际场景中,为了保证幂等,或者其他约束,往往会有唯一键,在批量插入的时候,往往需要唯一键冲突的时候,用新的值替换老的值,sql如下

INSERT INTO table (a,b,c) VALUES (1,2,3),(4,5,6)  
      ON DUPLICATE KEY UPDATE c=VALUES(a)+VALUES(b); 

不同的公司内部框架实现都不一样,但底层都是一样的sql,只是上层的语法不一样而已

批量插入控制一次批量的size

如果一次批量的数据太大,执行时间过长,容易导致失败,而且耗时高。所以,一般都是每次批量固定的个数,然后并发的执行,具体的批量个数可以根据实际场景决定。

var batchCnt = 200

group, errCtx := errgroup.WithContext(ctx)
	for len(convInfos) > batchCnt {
		batchList := convInfos[:batchCnt]
		convInfos = convInfos[batchCnt:]
		group.Go(
			func() error {
				return batchFunc(errCtx, batchList)
			})
	}
	group.Go(func() error {
		return batchFunc(errCtx, convInfos)
	})
	err := group.Wait()

	if err != nil {
		log.Errorf(ctx, "BatchAddXXXX errGroup failed, err %v", err)
		return err
	}

也可以写一个切分的函数,然后遍历结果并发执行

func Split(s []int, size int) [][]int {
	res := make([][]int, 0, len(s)/size+1)
	for len(s) > size {
		res = append(res, s[0:size])
		s = s[size:]
	}
	if len(s) > 0 {
		res = append(res, s[0:])
	}
	return res
}

errgroup的坑

代码一中:errgroup的Go方法,内部有一个闭包,这个闭包执行的时候,取的值是在另一个goroutine里面取了for这个goroutine的值,所以,代码一打印出来的值,不是完整的1-9。

代码二中:直接go p(i),对p方法的参数赋值这个操作,还是for这个goroutine来做的,所以能完整的打印出1-9

代码三中:对i进行了一个for循环内的引用,每次生成的闭包读的值都不一样,所以能完整的打印出1-9

代码一:

func TestErrgroup(t *testing.T) {
	group, _ := errgroup.WithContext(context.Background())
	for i := 0; i < 10; i++ {
		group.Go(func() error {
			fmt.Println(i)
			return nil
		})
	}
	group.Wait()
}

执行结果
=== RUN   TestErrgroup
10
10
10
10
10
10
10
10
10
--- PASS: TestErrgroup (0.00s)
10

代码二:

func TestErrgroup(t *testing.T) {
	for i := 0; i < 10; i++ {
		go p(i)
	}
}

func p(i int) {
	fmt.Println(i)
}

=== RUN   TestErrgroup
3
6
4
5
7
8
1
9
0
2
--- PASS: TestErrgroup (0.00s)
PASS

代码三:

func TestErrgroup(t *testing.T) {
	group, _ := errgroup.WithContext(context.Background())
	for i := 0; i < 10; i++ {
		k := i
		group.Go(func() error {
			fmt.Println(k)
			return nil
		})
	}
	group.Wait()
}

输出:
=== RUN   TestErrgroup
3
1
9
2
7
0
8
4
5
6
--- PASS: TestErrgroup (0.00s)

errgroup的取消

errgroup默认会话有一个任务返回err的时候,取消这个context,如果想要在一个任务报错的时候,取消其他并发执行的任务,则把这个context传进去。

func TestErrgroup(t *testing.T) {
	group, ctx := errgroup.WithContext(context.Background())
	for i := 0; i < 10; i++ {
		k := i
		group.Go(func() error {
			return p(ctx, k) //一个任务失败,其他并发任务会取消
		})
	}
	group.Wait()
}

func p(ctx context.Context, i int) error {
	select {
	case <-ctx.Done():
		return errors.New("err")
	default:
	}
	fmt.Println(i)
	return nil
}

如果不想取消,则重新生成一个context

func TestErrgroup(t *testing.T) {
	group, ctx := errgroup.WithContext(context.Background())
	for i := 0; i < 10; i++ {
		k := i
		group.Go(func() error {
			return p(context.Background(), k) //重新生成ctx,互不影响
		})
	}
	group.Wait()
}

func p(ctx context.Context, i int) error {
	select {
	case <-ctx.Done():
		return errors.New("err")
	default:
	}
	fmt.Println(i)
	return nil
}