go-redis先读后写并发竞争key的解决方案

redis本身是一个单线程的数据库,本身并不存在内部的竞争关系,但是在我们使用go-redis等中间件并发访问时会出现key的竞争问题。

如下代码,当需要“test"key的值<=1时才需要对其incr;我们需要先读取该值,再对该值进行判断后确定是否需要incr;

如果不对并发进行处理,会造成多个协程读取到<=1的值,进而重复incr造成数据错误:

var wg sync.WaitGroup

func main() {
	client := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		DB:       0,
		Password: "password",
	})
	_, err := client.Ping().Result()
	if err != nil {
		log.Fatal("连接redis失败", err.Error())
	}
	client.Set("test", 1, time.Hour*1)
	for i := 1; i <= 10; i++ {
		wg.Add(1)
		go func(i int) {
			val := client.Get("test").Val()
			fmt.Printf("第%d个协程获得的值:%s\n", i, val)
			a, _ := strconv.Atoi(val)
			if a <= 1 {
				client.Incr("test")
			}
			wg.Done()
		}(i)
	}
	wg.Wait()
	fmt.Printf("最终获得的值:%s\n", client.Get("test").Val())
}

执行以上代码会发现存在并发问题:

在这里插入图片描述

方法一、使用channel模拟锁控制

var (
	wg sync.WaitGroup
	lockChan = make(chan struct{}, 1)
)

// 如果lockChan中为空则阻塞
func getLock() {
	<-lockChan
}

// 重新填充lockChan
func releaseLock() {
	lockChan <- struct{}{}
}

func main() {
	// 初始化lockChan
	releaseLock()
	client := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		DB:       0,
		Password: "password",
	})
	_, err := client.Ping().Result()
	if err != nil {
		log.Fatal("连接redis失败", err.Error())
	}
	client.Set("test", 1, time.Hour*1)
	for i := 1; i <= 10; i++ {
		wg.Add(1)
		go func(i int) {
			getLock()
			val := client.Get("test").Val()
			fmt.Printf("第%d个协程获得的值:%s\n", i, val)
			a, _ := strconv.Atoi(val)
			if a <= 1 {
				client.Incr("test")
			}
			releaseLock()
			wg.Done()
		}(i)
	}
	wg.Wait()
	fmt.Printf("最终获得的值:%s\n", client.Get("test").Val())
}

方法二、setnx实现分布式锁

setnx只有在key不存在时,执行set;当key存在时,不进行操作。

命令设置成功返回1,失败时返回0。

例如当A抢到锁时,B返回0(go-redis中返回true或false),则B根据业务重试或者取消设置。

var (
	wg     sync.WaitGroup
	client *redis.Client
)

// 如果lockChan中为空则阻塞
func getLock() bool {
	// 设置适当的过期时间,若出现断连情况,redis会将超时的锁删除,防止死锁
	result, err := client.SetNX("lock", 1, time.Millisecond*1000*20).Result()
	if err != nil {
		fmt.Println(err)
	}
	//fmt.Println(result)
	return result
}

// 重新填充lockChan
func releaseLock() {
	client.Del("lock")
}

func main() {
	client = redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		DB:       0,
		Password: "Redis123",
	})
	_, err := client.Ping().Result()
	if err != nil {
		log.Fatal("连接redis失败", err.Error())
	}
	client.Set("test", 1, -1)
	for i := 1; i <= 10; i++ {
		wg.Add(1)
		go func(i int) {
      defer wg.Done()
		RETRY:
			lock := getLock()
			if !lock {
				// 取消设置
				//return
				// 重试
				goto RETRY
			}
			val := client.Get("test").Val()
			fmt.Printf("第%d个协程获得的值:%s\n", i, val)
			a, _ := strconv.Atoi(val)
			if a <= 1 {
				client.Incr("test")
			}
			releaseLock()
		}(i)
	}
	wg.Wait()
	fmt.Printf("最终获得的值:%s\n", client.Get("test").Val())
}

方法三、使用消息队列或Channel使并发串行化,将任务放入队列中依次执行

使用channel会有不可持久化、channel阻塞等问题(当执行命令协程获取不到足够多的时间来处理,并且channel的容量满足不了需求时,或造成通道的额阻塞,一种解决方法可以查看另一篇文章通过runtime的LockOSThread使goroutine优先执行)

channel实现:

var (
	wg     sync.WaitGroup
	client *redis.Client
	// 任务channel
	taskChan = make(chan *tasks, 1000)
	// 停止协程信号
	stopChan = make(chan struct{})
	// 此处用于模拟停止条件
	countChan = make(chan int)
)

type tasks struct {
	idx  int
	task func(int)
}

func stop() {
	defer wg.Done()
OUT:
	for {
		select {
		case c := <-countChan:
			if c >= 10 {
				stopChan <- struct{}{}
				break OUT
			}
		}
	}
}

/**
 *@Method 执行队列中的任务
 *@Params
 *@Return
 *@Tips:
 */
func doTask() {
	defer func() {
		close(taskChan)
		close(stopChan)
		wg.Done()
	}()
	// 用于计数模拟停止条件
	count := 0
OUT:
	for {
		select {
		case t := <-taskChan:
			t.task(t.idx)
			count++
			countChan <- count
		case <-stopChan:
			break OUT
		}
	}
}

/**
 *@Method 将任务加入队列
 *@Params
 *@Return
 *@Tips:
 */
func addTask(i int) {
	defer wg.Done()
	var t = tasks{
		idx: i,
		task: func(i int) {
			val := client.Get("test").Val()
			fmt.Printf("第%d个协程获得的值:%s\n", i, val)
			a, _ := strconv.Atoi(val)
			if a <= 1 {
				client.Incr("test")
			}
		},
	}
	taskChan <- &t
}

func main() {
	wg.Add(1)
	go stop()
	client = redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		DB:       0,
		Password: "Redis123",
	})
	_, err := client.Ping().Result()
	if err != nil {
		log.Fatal("连接redis失败", err.Error())
	}
	wg.Add(1)
	go doTask()
	client.Set("test", 1, -1)
	for i := 1; i <= 10; i++ {
		wg.Add(1)
		// 执行添加任务函数,将任务加入channel队列中
		go addTask(i)
	}
	wg.Wait()
	fmt.Printf("最终获得的值:%s\n", client.Get("test").Val())
}

未完待续

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值