记录一下乐观锁和悲观

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

以上是关于记录一下乐观锁和悲观的主要内容,如果未能解决你的问题,请参考以下文章

总结乐观锁和悲观锁

请说一下悲观锁和乐观锁的区别

订单并发处理--悲观锁和乐观锁任务队列

乐观锁和悲观锁

乐观锁和悲观锁

悲观锁和乐观锁的区别以及实现方式