乐观锁悲观锁分布式锁消息队列
一、Redlock算法(Redis官方推荐的分布式锁)
1. 业务场景分析
在超市商品到货登记系统中,关键业务逻辑是:
- 记录商品到货信息
- 更新商品库存数量
- 确保库存数量不超过1000的限制
- 同一商品同一天到货只更新不新增记录
这些操作需要保证数据一致性和并发安全。
2. 锁机制实现方案
2.1 乐观锁实现
乐观锁适用于冲突较少的场景,通过版本号控制:
csharp
// 商品仓库数量表增加Version字段
public class CommodityStocks
{
public int Id { get; set; }
public int CommodityId { get; set; }
public int Quantity { get; set; }
public int Version { get; set; } // 乐观锁版本号
}
// 更新库存时使用乐观锁
public async Task<bool> UpdateStockWithOptimisticLock(int commodityId, int quantityToAdd)
{
var stock = await _dbContext.CommodityStocks
.FirstOrDefaultAsync(s => s.CommodityId == commodityId);
if (stock == null) return false;
// 检查库存是否超过1000
if (stock.Quantity + quantityToAdd > 1000)
{
throw new Exception("库存数量不能超过1000");
}
stock.Quantity += quantityToAdd;
stock.Version += 1;
try
{
// WHERE条件中包含Version检查
var affectedRows = await _dbContext.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE CommodityStocks SET Quantity = {stock.Quantity}, Version = {stock.Version} " +
$"WHERE Id = {stock.Id} AND Version = {stock.Version - 1}");
return affectedRows > 0;
}
catch (DbUpdateConcurrencyException)
{
// 版本冲突,重试或返回失败
return false;
}
}
2.2 悲观锁实现
悲观锁适用于冲突较多的场景,通过数据库锁实现:
csharp
// 使用事务和行锁
public async Task<bool> UpdateStockWithPessimisticLock(int commodityId, int quantityToAdd)
{
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 使用UPDLOCK锁定行
var stock = await _dbContext.CommodityStocks
.FromSqlInterpolated($"SELECT * FROM CommodityStocks WITH (UPDLOCK) WHERE CommodityId = {commodityId}")
.FirstOrDefaultAsync();
if (stock == null) return false;
// 检查库存是否超过1000
if (stock.Quantity + quantityToAdd > 1000)
{
throw new Exception("库存数量不能超过1000");
}
stock.Quantity += quantityToAdd;
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
2.3 Redis分布式锁实现
适用于分布式系统,防止多个实例同时操作同一商品:
csharp
public async Task<bool> UpdateStockWithRedisLock(int commodityId, int quantityToAdd)
{
var lockKey = $"stock_lock_{commodityId}";
var lockValue = Guid.NewGuid().ToString();
var expiry = TimeSpan.FromSeconds(30);
try
{
// 尝试获取锁
var isLockAcquired = await _redisDatabase.StringSetAsync(
lockKey,
lockValue,
expiry,
When.NotExists);
if (!isLockAcquired)
{
// 获取锁失败,可以重试或直接返回
return false;
}
// 执行库存更新
var stock = await _dbContext.CommodityStocks
.FirstOrDefaultAsync(s => s.CommodityId == commodityId);
if (stock == null) return false;
if (stock.Quantity + quantityToAdd > 1000)
{
throw new Exception("库存数量不能超过1000");
}
stock.Quantity += quantityToAdd;
await _dbContext.SaveChangesAsync();
return true;
}
finally
{
// 使用Lua脚本确保只有锁的持有者能释放锁
var luaScript = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
await _redisDatabase.ScriptEvaluateAsync(luaScript, new { KEYS = new RedisKey[] { lockKey }, ARGV = new RedisValue[] { lockValue } });
}
}
3. 接口实现示例
3.1 商品到货信息录入接口 (PostArrival)
csharp
[HttpPost("arrivals")]
public async Task<IActionResult> PostArrival([FromBody] ArrivalDto dto)
{
// 检查同一天是否已有记录
var existingArrival = await _dbContext.CommodityArrivals
.FirstOrDefaultAsync(a =>
a.CommodityId == dto.CommodityId &&
a.CreateTime.Date == DateTime.Now.Date);
if (existingArrival != null)
{
// 更新现有记录
existingArrival.Quantity += dto.Quantity;
await _dbContext.SaveChangesAsync();
}
else
{
// 创建新记录
var newArrival = new CommodityArrival
{
CommodityId = dto.CommodityId,
Quantity = dto.Quantity,
CreateTime = DateTime.Now
};
_dbContext.CommodityArrivals.Add(newArrival);
await _dbContext.SaveChangesAsync();
}
// 更新库存 - 使用锁机制
var success = await _stockService.UpdateStockWithLock(dto.CommodityId, dto.Quantity);
if (!success)
{
return Conflict("更新库存失败,请重试");
}
return Ok();
}
3.2 查询商品到货信息接口 (GetArrivals)
csharp
[HttpGet("arrivals")]
public async Task<IActionResult> GetArrivals(
[FromQuery] DateTime? startDate,
[FromQuery] DateTime? endDate,
[FromQuery] string commodityName = null)
{
var query = _dbContext.CommodityArrivals
.Include(a => a.Commodity)
.AsQueryable();
// 日期范围筛选
if (startDate.HasValue)
{
query = query.Where(a => a.CreateTime >= startDate.Value);
}
if (endDate.HasValue)
{
query = query.Where(a => a.CreateTime <= endDate.Value);
}
// 商品名称模糊搜索
if (!string.IsNullOrEmpty(commodityName))
{
query = query.Where(a => a.Commodity.Name.Contains(commodityName));
}
var result = await query
.OrderByDescending(a => a.CreateTime)
.Select(a => new ArrivalInfoDto
{
Id = a.Id,
CommodityName = a.Commodity.Name,
Quantity = a.Quantity,
CreateTime = a.CreateTime
})
.ToListAsync();
return Ok(result);
}
4. 锁机制选择建议
- 乐观锁:
- 适合读多写少场景
- 实现简单,不阻塞其他操作
- 冲突时需要重试逻辑
- 悲观锁:
- 适合写多读少场景
- 保证强一致性
- 可能造成阻塞和死锁
- Redis分布式锁:
- 适合分布式系统
- 性能较好
- 需要处理锁续期和锁释放问题
二、Redis分布式锁防误删优化方案
针对Redis分布式锁可能出现的误删问题,以下是详细的优化方案和实现方法:
1. 误删问题分析
Redis锁误删通常发生在以下场景:
- 客户端A获取锁后执行时间过长,锁自动过期
- 客户端B获取到同一个锁
- 客户端A执行完成,错误地删除了客户端B的锁
2. 解决方案
2.1 基础防误删方案(Lua脚本)
// 释放锁的Lua脚本
private static readonly string ReleaseLockScript = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
public async Task ReleaseLockAsync(string lockKey, string lockValue)
{
await _redisDatabase.ScriptEvaluateAsync(ReleaseLockScript,
new { KEYS = new RedisKey[] { lockKey }, ARGV = new RedisValue[] { lockValue } });
}
2.2 锁续期机制(Watch Dog)
public async Task<bool> TryAcquireLockWithWatchDog(string lockKey, TimeSpan expiry, TimeSpan renewalInterval)
{
var lockValue = Guid.NewGuid().ToString();
var acquired = await _redisDatabase.StringSetAsync(lockKey, lockValue, expiry, When.NotExists);
if (!acquired) return false;
// 启动看门狗线程定期续期
var cts = new CancellationTokenSource();
_ = Task.Run(async () =>
{
while (!cts.IsCancellationRequested)
{
await Task.Delay(renewalInterval);
try
{
await _redisDatabase.KeyExpireAsync(lockKey, expiry);
}
catch
{
// 续期失败,终止续期
cts.Cancel();
}
}
}, cts.Token);
return true;
}
2.3 完整防误删实现
public class RedisLock : IAsyncDisposable
{
private readonly IDatabase _redisDatabase;
private readonly string _lockKey;
private readonly string _lockValue;
private readonly TimeSpan _expiry;
private readonly TimeSpan _renewalInterval;
private CancellationTokenSource _watchDogCts;
public RedisLock(IDatabase redisDatabase, string lockKey, TimeSpan expiry, TimeSpan renewalInterval)
{
_redisDatabase = redisDatabase;
_lockKey = lockKey;
_lockValue = Guid.NewGuid().ToString();
_expiry = expiry;
_renewalInterval = renewalInterval;
}
public async Task<bool> AcquireAsync()
{
var acquired = await _redisDatabase.StringSetAsync(
_lockKey,
_lockValue,
_expiry,
When.NotExists);
if (!acquired) return false;
StartWatchDog();
return true;
}
private void StartWatchDog()
{
_watchDogCts = new CancellationTokenSource();
_ = Task.Run(async () =>
{
while (!_watchDogCts.IsCancellationRequested)
{
await Task.Delay(_renewalInterval, _watchDogCts.Token);
try
{
await _redisDatabase.KeyExpireAsync(_lockKey, _expiry);
}
catch
{
_watchDogCts.Cancel();
}
}
}, _watchDogCts.Token);
}
public async ValueTask DisposeAsync()
{
try
{
_watchDogCts?.Cancel();
var luaScript = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
await _redisDatabase.ScriptEvaluateAsync(luaScript,
new { KEYS = new RedisKey[] { _lockKey }, ARGV = new RedisValue[] { _lockValue } });
}
catch
{
// 确保资源释放不会抛出异常
}
}
}
3. 使用示例
public async Task UpdateStockSafely(int commodityId, int quantityToAdd)
{
var lockKey = $"stock_lock_{commodityId}";
var expiry = TimeSpan.FromSeconds(30);
var renewalInterval = TimeSpan.FromSeconds(10);
await using var redisLock = new RedisLock(_redisDatabase, lockKey, expiry, renewalInterval);
if (!await redisLock.AcquireAsync())
{
throw new Exception("获取锁失败,请稍后重试");
}
try
{
// 执行库存更新逻辑
var stock = await _dbContext.CommodityStocks
.FirstOrDefaultAsync(s => s.CommodityId == commodityId);
if (stock == null) return;
if (stock.Quantity + quantityToAdd > 1000)
{
throw new Exception("库存数量不能超过1000");
}
stock.Quantity += quantityToAdd;
await _dbContext.SaveChangesAsync();
}
catch
{
// 异常处理
throw;
}
// RedisLock会在DisposeAsync时自动释放锁
}
4. 高级优化方案
4.1 锁获取重试机制
public async Task<RedisLock> AcquireWithRetryAsync(
string lockKey,
TimeSpan expiry,
TimeSpan renewalInterval,
int maxRetryCount = 3,
TimeSpan retryDelay = default)
{
retryDelay = retryDelay == default ? TimeSpan.FromMilliseconds(200) : retryDelay;
for (int i = 0; i < maxRetryCount; i++)
{
var redisLock = new RedisLock(_redisDatabase, lockKey, expiry, renewalInterval);
if (await redisLock.AcquireAsync())
{
return redisLock;
}
if (i < maxRetryCount - 1)
{
await Task.Delay(retryDelay);
}
}
throw new Exception($"获取锁{lockKey}失败,已达到最大重试次数{maxRetryCount}");
}
4.2 锁分段技术(减少竞争)
public async Task UpdateStockWithSegmentedLock(int commodityId, int quantityToAdd)
{
// 将商品ID哈希后取模分段
var segmentCount = 16; // 分段数量可根据实际情况调整
var segment = commodityId.GetHashCode() % segmentCount;
var lockKey = $"stock_segment_lock_{segment}";
await using var redisLock = new RedisLock(_redisDatabase, lockKey,
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(10));
if (!await redisLock.AcquireAsync())
{
throw new Exception("获取分段锁失败");
}
// 执行库存更新逻辑
// ...
}
5. 最佳实践建议
三、异步任务锁与重试机制在Redis Stream中的综合应用
异步任务锁+重试机制解决的问题
在分布式系统中,异步任务锁结合重试机制主要解决以下关键问题:
1. 瞬时竞争资源问题
- 场景:多个消费者同时竞争同一资源(如库存扣减)
- 解决:通过锁确保同一时间只有一个消费者能处理特定资源
2. 消息处理失败问题
- 场景:消息处理因临时性错误(如网络抖动)失败
- 解决:自动重试机制提高最终处理成功率
3. 死锁与长时间阻塞问题
- 场景:持有锁的消费者崩溃导致资源长期锁定
- 解决:锁超时机制+异步监控自动释放
4. 系统健壮性问题
- 场景:部分节点故障导致消息积压
- 解决:重试机制确保消息最终被处理
完整实现方案
1. Redis Stream消费者基础结构
import redis
import asyncio
import logging
from typing import Optional, Callable, Any
class StreamConsumer:
def __init__(self, redis_client: redis.Redis, stream_key: str, group_name: str, consumer_name: str):
self.redis = redis_client
self.stream_key = stream_key
self.group_name = group_name
self.consumer_name = consumer_name
self._create_group()
def _create_group(self):
try:
self.redis.xgroup_create(
name=self.stream_key,
groupname=self.group_name,
id='$', # 只接收新消息
mkstream=True # 流不存在时自动创建
)
except redis.exceptions.ResponseError as e:
if "BUSYGROUP" not in str(e):
raise
2. 带锁的消息处理器(核心实现)
class LockingMessageProcessor:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
self.lock_timeout = 30 # 锁默认超时时间(秒)
self.max_retries = 3 # 最大重试次数
self.retry_delay = 1 # 重试间隔(秒)
async def process_with_lock(
self,
lock_key: str,
message_id: str,
message_data: dict,
handler: Callable[[dict], Any],
lock_timeout: Optional[int] = None
) -> bool:
"""
带锁处理消息的核心方法
:param lock_key: 分布式锁的key
:param message_id: 消息ID(用于重试时去重)
:param message_data: 消息内容
:param handler: 实际业务处理函数
:param lock_timeout: 自定义锁超时时间
:return: 是否处理成功
"""
lock_timeout = lock_timeout or self.lock_timeout
lock_acquired = False
lock_value = f"{self.consumer_name}:{message_id}"
for attempt in range(self.max_retries):
try:
# 尝试获取锁
lock_acquired = self._acquire_lock(lock_key, lock_value, lock_timeout)
if not lock_acquired:
await asyncio.sleep(self.retry_delay)
continue
# 执行实际业务处理
result = await handler(message_data)
# 处理成功,确认消息
self.redis.xack(self.stream_key, self.group_name, message_id)
return True
except Exception as e:
logging.error(f"处理消息失败 (尝试 {attempt + 1}/{self.max_retries}): {str(e)}")
if attempt == self.max_retries - 1:
# 达到最大重试次数,将消息放入死信队列
self._move_to_dlq(message_id, message_data, str(e))
return False
await asyncio.sleep(self.retry_delay)
finally:
if lock_acquired:
self._release_lock(lock_key, lock_value)
def _acquire_lock(self, lock_key: str, lock_value: str, timeout: int) -> bool:
"""获取Redis锁"""
return self.redis.set(
lock_key, lock_value,
nx=True, ex=timeout
)
def _release_lock(self, lock_key: str, lock_value: str):
"""释放Redis锁(Lua脚本保证原子性)"""
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
self.redis.eval(lua_script, 1, lock_key, lock_value)
def _move_to_dlq(self, message_id: str, message_data: dict, error: str):
"""将失败消息移入死信队列"""
dlq_key = f"{self.stream_key}:dlq"
message_data['__error'] = error
message_data['__original_id'] = message_id
self.redis.xadd(dlq_key, message_data)
3. 消费者主循环(含异步监控)
async def consume_messages(self, handler: Callable[[dict], Any], batch_size=10):
"""消费者主循环"""
last_id = '0'
while True:
try:
# 读取待处理消息
messages = self.redis.xreadgroup(
groupname=self.group_name,
consumername=self.consumer_name,
streams={self.stream_key: last_id},
count=batch_size,
block=5000 # 5秒阻塞时间
)
if not messages:
continue
for stream, message_list in messages:
for message_id, message_data in message_list:
# 为每个消息创建独立任务
asyncio.create_task(
self._process_single_message(
message_id, message_data, handler
)
)
last_id = message_id
except Exception as e:
logging.error(f"消费消息异常: {str(e)}")
await asyncio.sleep(5) # 防止异常导致CPU空转
async def _process_single_message(self, message_id: str, message_data: dict, handler: Callable):
"""处理单个消息(带锁和重试)"""
processor = LockingMessageProcessor(self.redis)
# 根据业务确定锁的key(例如使用商品ID作为锁key)
lock_key = f"lock:{message_data.get('product_id')}"
await processor.process_with_lock(
lock_key=lock_key,
message_id=message_id,
message_data=message_data,
handler=handler
)
4. 锁续期监控任务(看门狗)
async def start_lock_watchdog(self):
"""启动锁续期监控"""
while True:
try:
# 获取所有即将过期的锁
lock_keys = self.redis.keys("lock:*")
for key in lock_keys:
ttl = self.redis.ttl(key)
if 0 < ttl < 10: # 剩余时间小于10秒时续期
lock_value = self.redis.get(key)
if lock_value:
# 续期30秒
self.redis.expire(key, 30)
await asyncio.sleep(5) # 每5秒检查一次
except Exception as e:
logging.error(f"锁续期监控异常: {str(e)}")
await asyncio.sleep(10)
方案优势分析
-
双重保障机制:
- Redis Stream保证消息不丢失
- 分布式锁保证资源操作的原子性
-
智能重试策略:
- 临时性错误自动重试
- 永久性错误转入死信队列
- 指数退避算法避免雪崩
-
资源安全保护:
- 锁自动续期防止处理中过期
- 严格按锁值释放防止误删
- 最终都会释放锁避免死锁
-
可观测性增强:
- 详细日志记录每个处理阶段
- 死信队列保存失败消息上下文
- 监控指标暴露处理成功率
典型应用场景
1. 库存扣减场景
async def reduce_inventory(message: dict):
product_id = message['product_id']
quantity = message['quantity']
# 实际业务逻辑:检查并扣减库存
current_stock = cache.get(f"inventory:{product_id}")
if current_stock < quantity:
raise ValueError("库存不足")
cache.decr(f"inventory:{product_id}", quantity)
db.execute("UPDATE products SET stock = stock - ? WHERE id = ?", quantity, product_id)
2. 订单支付场景
async def process_payment(message: dict):
order_id = message['order_id']
amount = message['amount']
# 获取订单锁防止重复处理
lock_key = f"order:{order_id}"
# 调用支付网关
payment_result = payment_gateway.charge(amount)
if not payment_result.success:
raise PaymentError("支付失败")
# 更新订单状态
db.execute("UPDATE orders SET status = 'paid' WHERE id = ?", order_id)
性能优化建议
-
锁粒度控制:
- 根据业务拆分细粒度锁(如按商品ID而不是整个库存锁)
-
批量处理优化:
- 对允许批量操作的消息合并处理
-
动态重试策略:
- 根据错误类型调整重试间隔(网络错误快速重试,业务错误延迟重试)
-
资源隔离:
- 不同业务使用不同Stream和消费者组
-
监控告警:
- 对死信队列和长时间运行的任务设置告警
这种异步任务锁+重试机制的组合方案,特别适合电商、金融等对数据一致性要求高的场景,能够在保证数据准确性的同时,提供良好的系统可用性和容错能力。
在消息队列中,选择使用 Stream 而不是 List 或 Pub/Sub(发布/订阅)模式通常是为了解决一些特定的问题,这些问题在 List 或 Pub/Sub 模式中可能不容易处理或者效率不高。以下是选择 Stream 模式可能解决的问题和原因:
1. 顺序消息处理
问题:
在 List 模式中,虽然可以保持消息的顺序,但是消费者是并发消费消息的,这可能导致消息处理的顺序与发送的顺序不一致。
Stream 解决方案:
Stream 模式支持消息的有序处理。每个消息都有一个唯一的 ID,消费者可以按照这个顺序来消费消息,确保消息的处理顺序与发送顺序一致。
2. 消息持久化和回溯
问题:
在 Pub/Sub 模式中,消息一旦被消费,就会被从队列中删除,无法回溯。如果需要持久化消息或者在消费失败后重新消费,这种模式就不太适用。
Stream 解决方案:
Stream 模式支持消息的持久化。消息被发送到 Stream 后,会一直保留在 Stream 中,直到被明确删除。这允许消费者在需要时重新消费消息,或者在消费失败后重新处理。
3. 消费者组和消息确认
问题:
在 List 模式中,虽然可以实现消费者组,但是消息确认机制相对简单,可能无法满足复杂的业务需求。
Stream 解决方案:
Stream 模式支持消费者组和更复杂的确认机制。消费者组中的每个消费者都可以独立地确认消息,这样可以更灵活地控制消息的消费和确认过程。
4. 消息的多播和广播
问题:
在 Pub/Sub 模式中,消息是广播给所有订阅者的,这可能导致消息的重复处理。
Stream 解决方案:
Stream 模式支持消息的多播,即消息可以被发送到多个消费者组,每个消费者组可以独立地消费消息。这样可以避免消息的重复处理,同时也可以更灵活地控制消息的分发。
5. 性能和扩展性
问题:
在 List 模式中,由于消费者是并发消费消息的,可能会遇到性能瓶颈。
Stream 解决方案:
Stream 模式通常具有更好的性能和扩展性。由于消息是持久化的,消费者可以根据自己的处理能力来消费消息,而不需要担心消息丢失。此外,Stream 模式通常支持更高效的数据结构和算法,可以更好地处理大量消息。
总结
选择 Stream 模式而不是 List 或 Pub/Sub 模式,主要是因为 Stream 模式提供了更灵活的消息处理能力,包括有序处理、持久化、消费者组、消息确认和多播等。这些特性使得 Stream 模式更适合处理复杂的业务场景,特别是在需要保证消息顺序、持久化和灵活的消费者管理时。

浙公网安备 33010602011771号