记录一下乐观锁和悲观
Posted jefreywo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记录一下乐观锁和悲观相关的知识,希望对你有一定的参考价值。
1、基本概念
并发场景下会出现数据竞争的问题,最后导致数据不一致,而乐观锁和悲观锁就是解决该类问题的两种不同的思路。
- 乐观锁:在操作数据时采取乐观态度,认为其他线程(协程)不会同时修改这部分数据,因此不会上锁,只是在执行更新操作时判断一下在此期间数据是否被其他线程(协程)修改了,如果数据已经被修改,则放弃更新操作,否则执行更新。
- 悲观锁:在操作数据时采取悲观态度,认为其他线程(协程)会同时修改数据。所以在操作数据时会将数据锁住,上锁期间不允许其他线程修改数据,一直到自己的操作全部完成后才会释放锁。
2、优缺点
乐观锁和悲观锁各有特点,有着各自适用的场景,不能认为这个优于另一个。
- 悲观锁的加锁机制可以很好保证数据安全,对于涉及敏感数据的修改可以采用悲观锁,另外悲观锁适用于并发冲突概率大、写比较多的场景,因为乐观锁在执行更新操作时频繁冲突(失败),会不断重试,浪费资源。 当然缺点也是有的,处理加锁的机制会让数据库产生额外的开销,还会有死锁的可能性。降低系统的吞吐量,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据,对于读比较多的场景优势就没那么明显了。
- 乐观锁本身是不加锁的(有时会与加锁配合,但本质是不会加锁的),只是在更新时判断一下数据是否被修改过,所以不会产生额外的数据库开销,而且不会限制并行性,比较适用于并发冲突概率小、读比较多的场景。 缺点也很明显,产生并发冲突时会重试,造成额外开销等。
3、实现方式
3.1 悲观锁
悲观锁的实现方式就是加锁,可以是对代码块加锁,也可以是对数据加锁(如mysql中的排它锁)。
3.2 乐观锁
乐观锁的常用实现方式有两种:版本号机制和CAS机制。
3.2.1 版本号机制
实现的基本思路:
- 增加一个version字段,表示数据的版本号,每当数据发生变更时,相应的version值也加1
- 每次获取数据的同时也将version的值拿出来(也可以再加上时间戳等)
- 当完成其他操作执行更新时,判断当前version与上一步读出来的version是否一致,如果一致则认为数据未被修改
- 如果version不一致,则更新失败,进行重试等策略
核心sql:
假设读取时 version = 5
UPDATE table SET status = ?, version = 5+1 WHERE key = ? AND version = 5;
golang 代码实践:
package main
import (
"errors"
"log"
"os"
"sync"
"time"
jefdb "github.com/jefreywo/golibs/db"
"gorm.io/gorm"
)
func main() {
db, err := jefdb.NewMysqlDB(&jefdb.MysqlDBConfig{
User: "root",
Password: "12345",
Host: "127.0.0.1",
Port: 3306,
Dbname: "test",
MaxIdleConns: 5,
MaxOpenConns: 80,
LogWriter: os.Stdout,
Colorful: true,
SlowThreshold: time.Second * 2,
LogLevel: "info",
})
if err != nil {
log.Fatalln(err)
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
err := updateUserBalance(db, 56)
if err != nil {
log.Println("updateUserBalance(100):", err)
}
}()
go func() {
defer wg.Done()
err := updateUserBalance(db, 123)
if err != nil {
log.Println("updateUserBalance(200):", err)
}
}()
wg.Wait()
}
var NoRowsAffectedError = errors.New("乐观锁更新数据失败")
func updateUserBalance(db *gorm.DB, reward int64) error {
// select时要把当前版本号取出
var u jefdb.JUser
if err := db.Select("id,balance,version").First(&u, 1).Error; err != nil {
return err
}
// 乐观锁更新失败时要重试,次数按实际需求设定
var retry = 3
var err error
for i := 0; i < retry; i++ {
err = db.Transaction(func(tx *gorm.DB) error {
// 其他事务操作
// 版本号更新
result := tx.Table("j_user").
Where("id = ? AND version = ?", u.Id, u.Version). // 判断版本号是否被更改
Updates(map[string]interface{}{
"balance": u.Balance + reward,
"version": u.Version + 1, // 版本号要+1
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
log.Println("更新失败, reward:", reward)
return NoRowsAffectedError
}
return nil
})
if err == nil {
break
} else {
if err == NoRowsAffectedError {
time.Sleep(time.Second)
continue
}
break
}
}
return err
}
3.2.2 CAS机制
以上是关于记录一下乐观锁和悲观的主要内容,如果未能解决你的问题,请参考以下文章