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 mainimport ( "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}