数据库锁

悲观锁

指的就是每次操作的时候,先把记录锁定起来,其他人无法操作这条记录

select * from user where user_id = 1 for update;

注意,数据库悲观锁使用时,一般伴随事务一起使用,数据锁定时间可能会很长,

需要根据实际情况选用。

乐观锁

就是利用版本号的概念,在操作前先获取到操作记录的当前version版本号,

然后操作的时候带上此版本号。

update user set age = age + 1, version = version + 1 where user_id = 2 and version = 1

注意,乐观锁主要使用于处理读多写少的问题。

在数据库中实现乐观锁时,通常需要显式创建版本号字段,这是实现乐观锁机制的核心要素。以下是关于版本号字段的必要性、实现方式及实践建议的详细说明:

一、为什么需要版本号字段?

乐观锁的核心逻辑是:在读取数据时记录当前版本,更新时检查版本是否一致,一致则更新并递增版本,否则认为数据已被修改(冲突)。
版本号字段的作用

  • 标识数据版本:记录数据的修改次数,每次更新时自动递增。
  • 冲突检测:通过比较客户端携带的版本号与数据库中的版本号,判断数据是否被其他线程修改。
  • 保证幂等性:同一操作多次执行时,版本号递增可避免重复修改(需结合业务逻辑)。

二、版本号字段的实现方式

1. 字段类型与命名规范

  • 常见字段名VersionRowVersionUpdateVersionTimestamp(注意:Timestamp在 SQL Server 中是二进制类型,需特殊处理)。
  • 推荐类型
    • 整数类型 (如intbigint):简单直观,适合计数场景,推荐使用。
    • 时间戳类型 (如datetimetimestamp):记录最后更新时间,但并发场景下可能因时间精度问题导致冲突漏检。

2. 数据库层面的实现

以 SQL Server 为例,创建带版本号的表:

sql

scss 复制代码
CREATE TABLE [UserInfo] (
    [Id] INT PRIMARY KEY IDENTITY(1,1),
    [Name] VARCHAR(50),
    [Age] INT,
    [Version] INT NOT NULL DEFAULT 1,  -- 初始版本为1
    [CreateTime] DATETIME DEFAULT GETDATE()
);

3. C# 代码中使用版本号实现乐观锁

以下是使用 Entity Framework Core(EF Core)结合版本号实现乐观锁的示例:

csharp

csharp 复制代码
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

// 数据模型(需标记版本号字段)
public class UserInfo
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    
    // 使用[ConcurrencyCheck]标记版本号字段,EF Core会自动处理冲突检测
    [ConcurrencyCheck]
    public int Version { get; set; }
    public DateTime CreateTime { get; set; }
}

// 数据库上下文
public class AppDbContext : DbContext
{
    public DbSet<UserInfo> UserInfos { get; set; }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=.;Database=TestDb;Trusted_Connection=True;");
    }
}

// 乐观锁更新操作示例
public class UserService
{
    private readonly AppDbContext _dbContext;

    public UserService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    // 带乐观锁的更新方法
    public async Task<bool> UpdateUserAgeWithOptimisticLock(int userId, int newAge, int expectedVersion)
    {
        try
        {
            // 1. 查询数据并指定版本号(模拟从客户端获取的版本)
            var user = await _dbContext.UserInfos
                .FirstOrDefaultAsync(u => u.Id == userId && u.Version == expectedVersion);
            
            if (user == null)
            {
                // 数据不存在或版本不一致,更新失败
                return false;
            }
            
            // 2. 更新数据
            user.Age = newAge;
            
            // 3. 保存更改,EF Core会自动生成"WHERE Version = expectedVersion"条件
            await _dbContext.SaveChangesAsync();
            return true;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            // 处理并发冲突:版本号不一致时抛出此异常
            Console.WriteLine("乐观锁冲突:数据已被其他操作修改");
            
            // 可选:获取数据库中的最新数据,返回给客户端重试
            var databaseEntry = ex.Entries.Single();
            var databaseValues = databaseEntry.GetDatabaseValues();
            if (databaseValues != null)
            {
                Console.WriteLine($"最新版本号:{databaseValues["Version"]}");
            }
            return false;
        }
    }
    
    // 另一种实现方式:使用EF Core的Update方法并手动处理版本
    public async Task<bool> UpdateUserAgeWithRawSql(int userId, int newAge, int expectedVersion)
    {
        // 使用原始SQL更新,显式指定版本号条件
        var affectedRows = await _dbContext.Database.ExecuteSqlRawAsync(
            "UPDATE UserInfo SET Age = @NewAge, Version = Version + 1 " +
            "WHERE Id = @UserId AND Version = @ExpectedVersion",
            new Microsoft.EntityFrameworkCore.Storage.SqlParameter("NewAge", newAge),
            new Microsoft.EntityFrameworkCore.Storage.SqlParameter("UserId", userId),
            new Microsoft.EntityFrameworkCore.Storage.SqlParameter("ExpectedVersion", expectedVersion)
        );
        
        // 受影响行数为1表示更新成功,0表示版本冲突或数据不存在
        return affectedRows == 1;
    }
}

三、不同数据库的版本号实现差异

数据库类型 版本号实现方式 注意事项
SQL Server INT类型字段,手动递增;或使用TIMESTAMP(二进制类型,需转换为字符串比较)。 TIMESTAMP不支持直接作为整数比较,推荐使用INT类型。
MySQL INT类型字段,手动递增;或使用DATETIME记录时间戳。 时间戳方案在高并发下可能因毫秒级精度导致冲突漏检,推荐使用整数版本号。
PostgreSQL INT类型字段,手动递增;或使用XMIN系统字段(事务 ID,适用于特定场景)。 XMIN依赖事务机制,使用前需了解其生命周期和局限性。
Oracle INT类型字段,手动递增;或使用ROWVERSION伪列(类似 SQL Server 的TIMESTAMP)。 ROWVERSION为二进制,需通过DBMS_ROWID.ROWID_RELATIVE_FNO等函数处理。

四、替代方案:时间戳(Timestamp)的局限性

  • 优势:时间戳自动记录更新时间,无需手动维护版本号。
  • 缺点
    • 精度问题:毫秒级时间戳在高并发场景下可能出现多个操作同一时间更新的情况,导致冲突漏检。
    • 时钟同步问题:分布式系统中服务器时钟不一致可能引发误判。
  • 适用场景:对并发冲突容忍度较高、更新频率较低的业务场景。

五、最佳实践建议

  1. 强制使用版本号字段 :在需要乐观锁的表中统一添加Version字段,初始值设为 1,更新时自动递增。
  2. 结合业务逻辑处理冲突
    • 冲突时返回错误信息,让客户端重试(适用于前端交互场景)。
    • 自动合并冲突数据(需业务逻辑支持,如合并更新字段)。
  1. 与幂等性结合:版本号可作为幂等令牌的辅助验证(如:令牌 + 版本号双重校验)。
  2. 索引优化:为版本号字段添加索引,提升查询和更新效率。

总结

乐观锁的版本号字段是实现并发控制的核心,必须在表中显式创建。通过数据库字段 + 代码逻辑的配合,可有效防止并发更新冲突,同时为幂等性实现提供支持。在实际开发中,应根据数据库类型选择合适的字段类型,并结合业务场景处理冲突情况。

相关推荐
l1t34 分钟前
用parser_tools插件来解析SQL语句
数据库·sql·插件·duckdb
TDengine (老段)1 小时前
TDengine 数学函数 ABS() 用户手册
大数据·数据库·sql·物联网·时序数据库·tdengine·涛思数据
Hello.Reader4 小时前
Apache StreamPark 快速上手从一键安装到跑起第一个 Flink SQL 任务
sql·flink·apache
养生技术人12 小时前
Oracle OCP认证考试题目详解082系列第57题
运维·数据库·sql·oracle·开闭原则
养生技术人17 小时前
Oracle OCP认证考试题目详解082系列第53题
数据库·sql·oracle·database·开闭原则·ocp
豆沙沙包?20 小时前
2025年--Lc171--H175 .组合两个表(SQL)
数据库·sql
养生技术人1 天前
Oracle OCP认证考试题目详解082系列第48题
运维·数据库·sql·oracle·database·开闭原则·ocp
工作中的程序员1 天前
hive sql优化基础
hive·sql
杨云龙UP1 天前
小工具大体验:rlwrap加持下的Oracle/MySQL/SQL Server命令行交互
运维·服务器·数据库·sql·mysql·oracle·sqlserver
阿巴~阿巴~1 天前
使用 C 语言连接 MySQL 客户端(重点)
服务器·数据库·sql·mysql·ubuntu