悲观锁
指的就是每次操作的时候,先把记录锁定起来,其他人无法操作这条记录
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. 字段类型与命名规范
- 常见字段名 :
Version
、RowVersion
、UpdateVersion
、Timestamp
(注意:Timestamp
在 SQL Server 中是二进制类型,需特殊处理)。 - 推荐类型:
-
- 整数类型 (如
int
、bigint
):简单直观,适合计数场景,推荐使用。 - 时间戳类型 (如
datetime
、timestamp
):记录最后更新时间,但并发场景下可能因时间精度问题导致冲突漏检。
- 整数类型 (如
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)的局限性
- 优势:时间戳自动记录更新时间,无需手动维护版本号。
- 缺点:
-
- 精度问题:毫秒级时间戳在高并发场景下可能出现多个操作同一时间更新的情况,导致冲突漏检。
- 时钟同步问题:分布式系统中服务器时钟不一致可能引发误判。
- 适用场景:对并发冲突容忍度较高、更新频率较低的业务场景。
五、最佳实践建议
- 强制使用版本号字段 :在需要乐观锁的表中统一添加
Version
字段,初始值设为 1,更新时自动递增。 - 结合业务逻辑处理冲突:
-
- 冲突时返回错误信息,让客户端重试(适用于前端交互场景)。
- 自动合并冲突数据(需业务逻辑支持,如合并更新字段)。
- 与幂等性结合:版本号可作为幂等令牌的辅助验证(如:令牌 + 版本号双重校验)。
- 索引优化:为版本号字段添加索引,提升查询和更新效率。
总结
乐观锁的版本号字段是实现并发控制的核心,必须在表中显式创建。通过数据库字段 + 代码逻辑的配合,可有效防止并发更新冲突,同时为幂等性实现提供支持。在实际开发中,应根据数据库类型选择合适的字段类型,并结合业务场景处理冲突情况。