全栈杂谈第16期:什么是乐观锁

166 阅读6分钟

概述

乐观锁(Optimistic Locking)是一种多线程或分布式环境下的并发控制策略。它认为并发冲突的概率较低,因此允许多个事务或线程在没有加锁的情况下并行操作数据。只有在提交数据时,才会检查是否存在冲突,如果发现冲突,则会回滚或重试。

乐观锁与悲观锁(Pessimistic Locking)不同,悲观锁通常会在数据操作前就对数据加锁,假设并发冲突的概率较高,因此锁会一直存在,直到事务或操作结束。而乐观锁在实际应用中,常常通过版本控制或时间戳等机制来实现。

乐观锁最常用于数据更新时,尤其是在数据库操作中,它能够有效提高性能,减少不必要的锁竞争。适用于那些读取多、写入少的场景。

乐观锁的实现方式

乐观锁的核心思想是:在数据读取时不加锁,而是在提交数据时检查是否有其他事务修改过数据。如果有冲突,则回滚事务或进行重试。如果没有冲突,则提交事务。实现乐观锁的常见方法有以下几种:

  1. 版本号控制 每个数据记录都包含一个版本号,读取数据时会读取版本号。更新数据时,除了更新数据本身,还需要检查当前版本号与数据库中的版本号是否一致。如果一致,则更新数据并将版本号加1;如果不一致,则说明有其他事务修改过数据,操作失败。
  2. 时间戳控制 每个数据记录都包含一个时间戳。读取数据时记录当前时间戳,更新数据时检查当前时间戳与数据库中的时间戳是否一致。如果一致,则更新数据并更新时间戳;如果不一致,则表示数据已被其他事务修改,操作失败。

乐观锁的应用场景

乐观锁适用于以下场景:

  • 高并发读少写多的场景:在这类场景中,读操作远远多于写操作,因此可以允许多个线程并行读操作,直到数据提交时再检查冲突。比如电子商务网站的商品库存、票务系统等。
  • 数据一致性要求较高的系统:例如金融系统中的交易操作,当多个用户同时进行购买操作时,乐观锁能有效避免数据冲突。
  • 分布式系统中的冲突解决:在微服务架构中,乐观锁能减少分布式事务中的锁竞争和性能瓶颈。

乐观锁的优缺点

优点:

  1. 减少锁竞争:乐观锁不需要加锁,因此减少了锁的争用,提高了系统的并发性能。
  2. 提高系统吞吐量:对于大多数读操作来说,乐观锁不需要加锁,因此能提高系统的吞吐量。
  3. 避免死锁:由于没有锁竞争,乐观锁自然避免了死锁的发生。

缺点:

  1. 重试机制复杂:如果乐观锁检测到冲突,通常需要回滚并重试,这可能会增加系统的复杂度。
  2. 写操作失败率较高:在高并发环境下,乐观锁的失败概率会增大,可能需要频繁重试。
  3. 不适用于写多读少的场景:在写操作频繁的场景下,乐观锁的优势会减弱,反而可能导致大量的冲突。

代码示例

下面分别用 Python、Go 和 Java 三种编程语言实现乐观锁。

Python

在 Python 中,我们可以通过 SQLAlchemypeewee 等 ORM 框架来实现乐观锁,以下是一个使用 SQLAlchemy 实现的乐观锁示例:

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.exc import StaleDataError

Base = declarative_base()

class Item(Base):
    __tablename__ = 'items'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    quantity = Column(Integer)
    version = Column(Integer)

# 创建数据库连接

engine = create_engine('sqlite:///items.db')
Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()

# 插入示例数据

item = Item(name="Laptop", quantity=10, version=1)
session.add(item)
session.commit()

# 模拟乐观锁更新操作

def update_item(item_id, new_quantity, version):
    try:
        item = session.query(Item).filter(Item.id == item_id).one()
        if item.version != version:
            raise StaleDataError("Version conflict, data has been modified.")
        

        item.quantity = new_quantity
        item.version += 1  # 更新版本号
        session.commit()
    except StaleDataError as e:
        print(f"Error: {e}")

# 尝试更新

update_item(1, 5, 1)  # 正常更新
update_item(1, 3, 2)  # 会抛出版本冲突异常

Go

在 Go 中,假设使用 gorm 作为 ORM 框架,也可以实现类似的乐观锁:

package main

import (
	"fmt"
	"log"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/sqlite"
)

type Item struct {
	ID       uint   `gorm:"primary_key"`
	Name     string
	Quantity int
	Version  int `gorm:"default:1"`
}

func main() {
	// 连接数据库
	db, err := gorm.Open("sqlite3", "./items.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 自动迁移数据库
	db.AutoMigrate(&Item{})
	
	// 插入示例数据
	item := Item{Name: "Laptop", Quantity: 10}
	db.Create(&item)
	
	// 乐观锁更新
	updateItem(db, 1, 5, 1)  // 正常更新
	updateItem(db, 1, 3, 2)  // 版本冲突

}

func updateItem(db *gorm.DB, id uint, newQuantity int, version int) {
	var item Item
	if err := db.First(&item, id).Error; err != nil {
		log.Println(err)
		return
	}

	if item.Version != version {
		fmt.Println("Version conflict, data has been modified.")
		return
	}
	
	// 更新数据
	item.Quantity = newQuantity
	item.Version++ // 更新版本号
	if err := db.Save(&item).Error; err != nil {
		fmt.Println("Failed to update item:", err)
		return
	}
	
	fmt.Println("Item updated successfully")

}

Java

在 Java 中,我们通常使用 Hibernate 或 JPA 来实现乐观锁。以下是一个使用 JPA 实现乐观锁的例子:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Version;
import javax.persistence.EntityManager;
import javax.persistence.Persistence;

@Entity
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int quantity;
    
    @Version
    private int version;  // 乐观锁版本号
    
    // Getters and Setters

}

public class OptimisticLockExample {

    public static void main(String[] args) {
        EntityManager em = Persistence.createEntityManagerFactory("examplePU").createEntityManager();
        em.getTransaction().begin();
    
        Item item = new Item();
        item.setName("Laptop");
        item.setQuantity(10);
        em.persist(item);
        em.getTransaction().commit();
    
        // 模拟乐观锁更新操作
        try {
            updateItem(em, item.getId(), 5, item.getVersion());
            updateItem(em, item.getId(), 3, item.getVersion() + 1);  // 版本冲突
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
    
    public static void updateItem(EntityManager em, Long id, int newQuantity, int version) {
        try {
            em.getTransaction().begin();
            Item item = em.find(Item.class, id);
    
            if (item.getVersion() != version) {
                throw new RuntimeException("Version conflict, data has been modified.");
            }
    
            item.setQuantity(newQuantity);
            item.setVersion(version + 1);
            em.getTransaction().commit();
    
            System.out.println("Item updated successfully");
        } catch (Exception e) {
            em.getTransaction().rollback();
            throw new RuntimeException("Failed to update item: " + e.getMessage());
        }
    }

}

总结

乐观锁是一种非常有效的并发控制策略,尤其适用于读多写少的应用场景。通过版本控制或时间戳机制,乐观锁能够在保证数据一致性的同时,最大限度地提高系统的并发性能。尽管它的失败概率在高并发写操作的场景中较高,但在许多现代应用中,它仍然是一种非常有用的并发处理方式。

在实际开发中,根据业务需求和系统负载选择合适的并发控制方式非常重要。如果应用场景适合乐观锁,合理地实现乐观锁将大大提高系统的吞吐量和性能。

欢迎关注公众号:“全栈开发指南针” 这里是技术潮流的风向标,也是你代码旅程的导航仪!🚀 Let’s code and have fun! 🎉