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())
}