记录一下乐观锁和悲观

156 阅读3分钟

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机制