【C#】从一次异步锁逐渐展开浅谈服务器架构解决重复编码问题,我与AI的一次深度讨论得出的一些解决方案

🌹欢迎来到《小5讲堂》🌹

🌹这是《C#》系列文章,每篇文章将以博主理解的角度展开讲解。🌹

🌹温馨提示:博主能力有限,理解水平有限,若有不对之处望指正!🌹

目录

前言

说实在的,博主虽然工作很多年,但是有些基础的概念和技术点其实没有太深入思考和总结。

所以,博主就会对这些技术点没有太多的理解和认识,当有些朋友问我的时候我就没能很好的表达。

特别是面试的时候,会给面试官很疑惑的感觉,哈哈哈,感觉你有些基础知识点好多不懂,

但是你又会0到1的开发,也不至于技术那么差。

好了,来看看博主是如何一步步和AI深度讨论得到一个解决方案的。

博主是从一个异步锁的知识点开始扩展到分布式锁或者说是多服务器解决重复编码问题。

说实在,博主确实没有分布式以及分布式redis锁的实战经验,但是大概概念是有的,

尤其是有了AI后,博主能够快速获得答案和理解其中的意思,AI是一个很好的辅助工具。

异步锁并发问题

博主的第一个提问是:

.net core 6.0,异步锁,单服务器10个并发查询同一个表的编码时,能挡得住吗

我觉得回答的挺好,至少知道单服务器大概的并发数是多少,当然这个还取决你服务器的配置。

在 .NET Core 6.0 中,处理10个并发查询同一个表的编码场景时,异步锁的使用效果取决于你选择的锁机制和实现方式。让我详细分析一下:

1. 异步锁的选择和性能

SemaphoreSlim(推荐)

csharp 复制代码
public class CodeService
{
    private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    
    public async Task<string> GetNextCodeAsync()
    {
        await _semaphore.WaitAsync();
        try
        {
            // 查询数据库并生成新编码
            return await GenerateNewCodeAsync();
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

性能分析

  • 10个并发:SemaphoreSlim 完全可以胜任
  • 吞吐量:在单服务器上,SemaphoreSlim 可以处理数千个并发请求
  • 内存占用:非常低,每个 SemaphoreSlim 实例约 24-40 字节

2. 实际测试示例

csharp 复制代码
[ApiController]
[Route("api/[controller]")]
public class CodeController : ControllerBase
{
    private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    private readonly ApplicationDbContext _context;

    public CodeController(ApplicationDbContext context)
    {
        _context = context;
    }

    [HttpPost("generate")]
    public async Task<ActionResult<string>> GenerateCode()
    {
        await _semaphore.WaitAsync();
        try
        {
            // 模拟一些处理时间
            await Task.Delay(10);
            
            // 查询当前最大编码
            var maxCode = await _context.Codes
                .OrderByDescending(c => c.Id)
                .Select(c => c.Code)
                .FirstOrDefaultAsync();

            // 生成新编码
            var newCode = GenerateNextCode(maxCode);
            
            // 保存到数据库
            var codeEntity = new Code { Code = newCode };
            _context.Codes.Add(codeEntity);
            await _context.SaveChangesAsync();

            return newCode;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    private string GenerateNextCode(string currentMaxCode)
    {
        if (string.IsNullOrEmpty(currentMaxCode))
            return "CODE000001";

        var numberPart = currentMaxCode.Substring(4);
        if (int.TryParse(numberPart, out int number))
        {
            return $"CODE{(number + 1).ToString("D6")}";
        }
        
        return "CODE000001";
    }
}

3. 性能基准测试

csharp 复制代码
[MemoryDiagnoser]
public class CodeLockBenchmark
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    private readonly HttpClient _client = new HttpClient();

    [Benchmark]
    public async Task TenConcurrentRequests()
    {
        var tasks = new List<Task>();
        
        for (int i = 0; i < 10; i++)
        {
            tasks.Add(Task.Run(async () =>
            {
                await _semaphore.WaitAsync();
                try
                {
                    // 模拟编码生成工作
                    await Task.Delay(10);
                }
                finally
                {
                    _semaphore.Release();
                }
            }));
        }

        await Task.WhenAll(tasks);
    }
}

4. 更高级的解决方案

分布式场景考虑

如果将来需要扩展到多服务器,可以考虑:

csharp 复制代码
// 使用 Redis 分布式锁
public class DistributedCodeService
{
    private readonly IDistributedLock _distributedLock;
    
    public async Task<string> GetNextCodeAsync()
    {
        var resource = "code_generation_lock";
        var expiry = TimeSpan.FromSeconds(30);
        
        await using (var redLock = await _distributedLock.CreateLockAsync(resource, expiry))
        {
            if (redLock.IsAcquired)
            {
                // 生成编码的逻辑
                return await GenerateNewCodeAsync();
            }
            else
            {
                throw new Exception("无法获取锁");
            }
        }
    }
}

5. 结论

对于单服务器10个并发查询的场景:

完全可以挡得住,而且性能很好:

  • SemaphoreSlim 在 .NET Core 6.0 中经过高度优化
  • 10个并发对现代服务器来说是很轻的负载
  • 异步操作不会阻塞线程池线程
  • 响应时间主要取决于数据库查询时间,而不是锁的开销

建议:

  1. 使用 SemaphoreSlim 作为首选
  2. 锁的持有时间要尽量短
  3. 使用 try-finally 确保锁总是被释放
  4. 考虑设置合理的超时时间

这种方案在单服务器环境下处理10个并发绰绰有余,即使扩展到100个并发也能良好工作。

多服务器锁问题

博主接着追问,第二个问题,有时候提问的对不对取决于你掌握的知识深浅情况,对于技术点理解深,自然问的就很到位,要么就像博主这样只能多轮提问得到正确理解的答案。

redis分布式锁的原因,我有A/B/C三台服务器,redis怎么设计

在A/B/C三台服务器的分布式环境下,使用Redis分布式锁是必要的。让我详细解释原因和设计方案:

1. 为什么需要Redis分布式锁

问题场景

csharp 复制代码
// 如果不使用分布式锁,每台服务器都有自己的锁
// 服务器A:SemaphoreSlim(只能控制A服务器的线程)
// 服务器B:SemaphoreSlim(只能控制B服务器的线程)
// 服务器C:SemaphoreSlim(只能控制C服务器的线程)

// 结果:三台服务器可能同时生成重复的编码

竞态条件示例

复制代码
时间点   服务器A       服务器B       服务器C       数据库状态
t1      查询最大编码                 当前最大: CODE000100
t2                  查询最大编码     当前最大: CODE000100  
t3                             查询最大编码 当前最大: CODE000100
t4      生成CODE000101
t5                  生成CODE000101  ← 重复!
t6                             生成CODE000101  ← 重复!

2. Redis分布式锁设计方案

使用 StackExchange.Redis

csharp 复制代码
public class RedisDistributedLock
{
    private readonly IConnectionMultiplexer _redis;
    private readonly string _lockKey;
    private readonly TimeSpan _expiry;
    private string _lockValue;

    public RedisDistributedLock(IConnectionMultiplexer redis, string lockKey, TimeSpan expiry)
    {
        _redis = redis;
        _lockKey = lockKey;
        _expiry = expiry;
    }

    public async Task<bool> AcquireAsync()
    {
        var db = _redis.GetDatabase();
        _lockValue = Guid.NewGuid().ToString();
        
        // 使用SET命令的NX和EX参数实现原子操作
        return await db.StringSetAsync(
            _lockKey, 
            _lockValue, 
            _expiry, 
            When.NotExists, 
            CommandFlags.None);
    }

    public async Task ReleaseAsync()
    {
        var db = _redis.GetDatabase();
        
        // 使用Lua脚本确保只有锁的持有者才能释放
        var script = @"
            if redis.call('GET', KEYS[1]) == ARGV[1] then
                return redis.call('DEL', KEYS[1])
            else
                return 0
            end";
        
        await db.ScriptEvaluateAsync(
            script, 
            new RedisKey[] { _lockKey }, 
            new RedisValue[] { _lockValue });
    }
}

完整的编码服务实现

csharp 复制代码
public class DistributedCodeService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ApplicationDbContext _context;
    private readonly ILogger<DistributedCodeService> _logger;

    public DistributedCodeService(
        IConnectionMultiplexer redis,
        ApplicationDbContext context,
        ILogger<DistributedCodeService> logger)
    {
        _redis = redis;
        _context = context;
        _logger = logger;
    }

    public async Task<string> GenerateCodeAsync()
    {
        var lockKey = "code_generation:lock";
        var expiry = TimeSpan.FromSeconds(30);
        var retryDelay = TimeSpan.FromMilliseconds(100);
        var maxRetryCount = 10;

        var distributedLock = new RedisDistributedLock(_redis, lockKey, expiry);

        for (int i = 0; i < maxRetryCount; i++)
        {
            if (await distributedLock.AcquireAsync())
            {
                try
                {
                    _logger.LogInformation("成功获取分布式锁,服务器: {Server}", Environment.MachineName);
                    return await GenerateCodeInternalAsync();
                }
                finally
                {
                    await distributedLock.ReleaseAsync();
                    _logger.LogInformation("释放分布式锁,服务器: {Server}", Environment.MachineName);
                }
            }

            _logger.LogWarning("获取锁失败,第{RetryCount}次重试", i + 1);
            await Task.Delay(retryDelay);
        }

        throw new Exception("无法获取分布式锁,请稍后重试");
    }

    private async Task<string> GenerateCodeInternalAsync()
    {
        // 查询当前最大编码
        var maxCode = await _context.Codes
            .OrderByDescending(c => c.Id)
            .Select(c => c.Code)
            .FirstOrDefaultAsync();

        // 生成新编码
        var newCode = GenerateNextCode(maxCode);

        // 保存到数据库
        var codeEntity = new Code { Code = newCode, CreatedAt = DateTime.UtcNow };
        _context.Codes.Add(codeEntity);
        await _context.SaveChangesAsync();

        return newCode;
    }

    private string GenerateNextCode(string currentMaxCode)
    {
        if (string.IsNullOrEmpty(currentMaxCode))
            return "CODE000001";

        var numberPart = currentMaxCode.Substring(4);
        if (int.TryParse(numberPart, out int number))
        {
            return $"CODE{(number + 1).ToString("D6")}";
        }

        return "CODE000001";
    }
}

3. Redis配置和部署方案

Redis部署模式选择

csharp 复制代码
// Program.cs 中的配置
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var configuration = ConfigurationOptions.Parse("redis_server:6379");
    configuration.AbortOnConnectFail = false;
    configuration.ConnectRetry = 3;
    configuration.ConnectTimeout = 5000;
    configuration.SyncTimeout = 5000;
    
    return ConnectionMultiplexer.Connect(configuration);
});

Redis高可用方案

csharp 复制代码
// Redis哨兵模式配置
var configuration = ConfigurationOptions.Parse("redis_sentinel_1:26379,redis_sentinel_2:26379,redis_sentinel_3:26379");
configuration.ServiceName = "mymaster";
configuration.AbortOnConnectFail = false;
configuration.ConnectRetry = 3;

4. 控制器使用示例

csharp 复制代码
[ApiController]
[Route("api/[controller]")]
public class CodeController : ControllerBase
{
    private readonly DistributedCodeService _codeService;

    public CodeController(DistributedCodeService codeService)
    {
        _codeService = codeService;
    }

    [HttpPost("generate")]
    public async Task<ActionResult<string>> GenerateCode()
    {
        try
        {
            var code = await _codeService.GenerateCodeAsync();
            return Ok(new { code, generatedBy = Environment.MachineName });
        }
        catch (Exception ex)
        {
            return StatusCode(503, new { error = ex.Message });
        }
    }
}

5. 监控和容错设计

锁竞争监控

csharp 复制代码
public class LockMetrics
{
    private readonly Counter _lockAcquisitionCounter;
    private readonly Counter _lockTimeoutCounter;

    public LockMetrics()
    {
        _lockAcquisitionCounter = Metrics.CreateCounter("distributed_lock_acquisitions_total", 
            "Number of lock acquisitions", new CounterConfiguration
            {
                LabelNames = new[] { "status" }
            });

        _lockTimeoutCounter = Metrics.CreateCounter("distributed_lock_timeouts_total", 
            "Number of lock timeouts");
    }

    public void RecordLockAcquisition(bool success)
    {
        _lockAcquisitionCounter.WithLabels(success ? "success" : "failure").Inc();
    }

    public void RecordLockTimeout()
    {
        _lockTimeoutCounter.Inc();
    }
}

降级策略

csharp 复制代码
public class CodeServiceWithFallback
{
    public async Task<string> GenerateCodeWithFallbackAsync()
    {
        try
        {
            // 优先使用分布式锁
            return await _distributedCodeService.GenerateCodeAsync();
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "分布式锁失败,使用本地降级方案");
            
            // 降级到基于数据库的乐观锁或时间戳方案
            return await GenerateCodeWithDatabaseLockAsync();
        }
    }
}

总结

Redis分布式锁设计要点:

  1. 唯一标识:使用GUID确保只有锁的持有者才能释放
  2. 自动过期:设置合理的过期时间防止死锁
  3. 原子操作:使用Lua脚本确保操作的原子性
  4. 重试机制:实现指数退避等重试策略
  5. 监控告警:添加监控指标和日志

这种设计可以确保在A/B/C三台服务器环境下,编码生成的唯一性和正确性。

共享状态

由于博主还没理解到位,继续接着提问:

这样的redis设计,在每个服务器下都有单独一套代码,不是三个不同线程吗,不会也是一样会出现同时获取重复编码吗,还是说这个redis代码,在三个独立服务器上代码会有判断?

您提出了一个很好的问题!让我澄清这个关键点:Redis分布式锁之所以能工作,正是因为它是一个中心化的共享状态,三个服务器上的代码都会连接到同一个Redis实例进行协调。

关键区别:共享状态 vs 独立状态

错误的本地锁(三套独立代码)

csharp 复制代码
// 服务器A
private static SemaphoreSlim _lockA = new SemaphoreSlim(1, 1);

// 服务器B  
private static SemaphoreSlim _lockB = new SemaphoreSlim(1, 1);

// 服务器C
private static SemaphoreSlim _lockC = new SemaphoreSlim(1, 1);

问题:每个服务器都有自己的锁对象,互相不知道对方的状态

正确的Redis分布式锁(共享状态)

csharp 复制代码
// 服务器A、B、C都连接到同一个Redis
private readonly IConnectionMultiplexer _redis; // 指向同一个Redis实例

// 所有服务器都使用相同的锁键名
var lockKey = "code_generation:lock"; 

Redis分布式锁的工作原理

执行流程

复制代码
服务器A → Redis: SET lock_key UUID123 NX EX 30
Redis → 服务器A: OK (获取锁成功)

服务器B → Redis: SET lock_key UUID456 NX EX 30  
Redis → 服务器B: nil (获取锁失败,因为A已经持有)

服务器C → Redis: SET lock_key UUID789 NX EX 30
Redis → 服务器C: nil (获取锁失败,因为A已经持有)

服务器A → 完成业务 → Redis: DEL lock_key (但只在自己持有时才删除)

实际代码演示

csharp 复制代码
// 这个代码在三个服务器上是一样的,但通过Redis协调
public async Task<string> GenerateCodeAsync()
{
    var lockKey = "code_generation:lock"; // 三个服务器使用相同的键名
    var expiry = TimeSpan.FromSeconds(30);
    
    // 所有服务器都连接到同一个Redis实例
    var db = _redis.GetDatabase();
    
    // 关键:SET NX EX 是原子操作,只有一个服务器能成功
    bool lockAcquired = await db.StringSetAsync(
        lockKey, 
        Guid.NewGuid().ToString(), 
        expiry, 
        When.NotExists);  // ← NX参数确保只有一个能设置成功
    
    if (lockAcquired)
    {
        try
        {
            // 只有获取锁的服务器能执行这里
            return await GenerateCodeInternalAsync();
        }
        finally
        {
            await ReleaseLockAsync(db, lockKey);
        }
    }
    else
    {
        // 其他服务器会等待或重试
        throw new Exception("请稍后重试");
    }
}

更完整的重试机制

csharp 复制代码
public async Task<string> GenerateCodeWithRetryAsync()
{
    var lockKey = "code_generation:lock";
    var lockValue = Guid.NewGuid().ToString();
    var expiry = TimeSpan.FromSeconds(30);
    var maxRetryTime = TimeSpan.FromSeconds(10);
    
    var db = _redis.GetDatabase();
    var stopwatch = Stopwatch.StartNew();
    
    while (stopwatch.Elapsed < maxRetryTime)
    {
        // 尝试获取锁
        bool acquired = await db.StringSetAsync(lockKey, lockValue, expiry, When.NotExists);
        
        if (acquired)
        {
            try
            {
                _logger.LogInformation($"服务器 {Environment.MachineName} 成功获取锁");
                return await GenerateCodeInternalAsync();
            }
            finally
            {
                await ReleaseLockAsync(db, lockKey, lockValue);
            }
        }
        
        // 等待一段时间后重试
        await Task.Delay(TimeSpan.FromMilliseconds(100 + new Random().Next(100)));
    }
    
    throw new TimeoutException("获取分布式锁超时");
}

private async Task ReleaseLockAsync(IDatabase db, string lockKey, string lockValue)
{
    // 使用Lua脚本确保只有锁的持有者才能释放
    var script = @"
        if redis.call('GET', KEYS[1]) == ARGV[1] then
            return redis.call('DEL', KEYS[1])
        else
            return 0
        end";
    
    await db.ScriptEvaluateAsync(script, new RedisKey[] { lockKey }, new RedisValue[] { lockValue });
}

实际测试验证

让我们模拟三个服务器的并发请求:

csharp 复制代码
// 模拟三个服务器的并发请求
[Test]
public async Task TestThreeServersConcurrent()
{
    var tasks = new List<Task>();
    var results = new ConcurrentBag<string>();
    var redis = ConnectionMultiplexer.Connect("localhost");
    
    // 模拟服务器A、B、C同时请求
    for (int i = 0; i < 3; i++)
    {
        tasks.Add(Task.Run(async () =>
        {
            var service = new DistributedCodeService(redis, dbContext, logger);
            var code = await service.GenerateCodeWithRetryAsync();
            results.Add(code);
        }));
    }
    
    await Task.WhenAll(tasks);
    
    // 验证:所有编码应该都是唯一的
    Assert.AreEqual(3, results.Distinct().Count());
}

总结

关键理解点:

  1. 共享状态 :三个服务器连接到同一个Redis实例
  2. 原子操作SET NX EX 确保只有一个服务器能成功设置键值
  3. 中心化协调:Redis作为中心协调者,确保同一时间只有一个服务器能获取锁
  4. 相同代码,协调执行:虽然代码在三个服务器上相同,但通过Redis的协调,它们不会同时执行关键代码段

所以Redis分布式锁能够确保在分布式环境下,即使有多个服务器、多个进程、多个线程,同一时间也只有一个能进入临界区,从而避免重复编码的问题。

独立部署

从DS回答,博主大概明白什么意思,所以接着继续提问,确保我的理解就是回答的:

如果像你说的共享状态,那么这个redis是怎么部署,不是每个服务器上都会生成一个redis实力吗,怎么确保三台独立服务器执行同一个redis实例

您指出了一个非常关键的问题!Redis确实是需要独立部署的,不是在每个应用服务器上各部署一个。让我详细解释部署架构:

1. Redis部署架构

错误的部署方式(您担心的)

复制代码
服务器A: 应用代码 + Redis实例A
服务器B: 应用代码 + Redis实例B  
服务器C: 应用代码 + Redis实例C

❌ 每个Redis实例独立,无法协调

正确的部署方式

复制代码
服务器A: 应用代码 → 连接 → 独立的Redis服务器
服务器B: 应用代码 → 连接 → 独立的Redis服务器  
服务器C: 应用代码 → 连接 → 独立的Redis服务器
                     ↓
             同一个Redis实例(中心化)

2. 实际部署方案

方案1:独立Redis服务器(推荐)

bash 复制代码
# 在一台独立的服务器上部署Redis
# Redis服务器: 192.168.1.100:6379

# 三台应用服务器的连接配置相同
Server A (192.168.1.10) → Redis (192.168.1.100:6379)
Server B (192.168.1.11) → Redis (192.168.1.100:6379)  
Server C (192.168.1.12) → Redis (192.168.1.100:6379)

应用服务器配置

csharp 复制代码
// 在三台服务器上使用相同的配置
// appsettings.json
{
  "Redis": {
    "ConnectionString": "192.168.1.100:6379,password=your_password,abortConnect=false"
  }
}

// Program.cs
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var connectionString = builder.Configuration["Redis:ConnectionString"];
    return ConnectionMultiplexer.Connect(connectionString);
});

3. Redis部署实践

Docker部署Redis

bash 复制代码
# 在独立的Redis服务器上运行
docker run -d \
  --name redis \
  -p 6379:6379 \
  -v /data/redis:/data \
  redis:7-alpine \
  redis-server --requirepass "your_secure_password"

配置文件方式

bash 复制代码
# redis.conf
bind 0.0.0.0
port 6379
requirepass your_secure_password
maxmemory 1gb
maxmemory-policy allkeys-lru

# 启动
redis-server /path/to/redis.conf

4. 高可用方案(生产环境)

Redis哨兵模式

bash 复制代码
# 3台Redis服务器 + 3台哨兵服务器
Redis Master: 192.168.1.100:6379
Redis Replica1: 192.168.1.101:6379  
Redis Replica2: 192.168.1.102:6379

Sentinel1: 192.168.1.110:26379
Sentinel2: 192.168.1.111:26379
Sentinel3: 192.168.1.112:26379

应用连接哨兵配置

csharp 复制代码
var configuration = new ConfigurationOptions
{
    EndPoints = 
    {
        { "192.168.1.110", 26379 },
        { "192.168.1.111", 26379 },
        { "192.168.1.112", 26379 }
    },
    ServiceName = "mymaster",
    Password = "your_secure_password",
    AbortOnConnectFail = false,
    ConnectRetry = 3,
    ConnectTimeout = 5000
};

var redis = ConnectionMultiplexer.Connect(configuration);

5. 验证部署正确性

测试脚本验证

csharp 复制代码
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
    private readonly IConnectionMultiplexer _redis;

    public TestController(IConnectionMultiplexer redis)
    {
        _redis = redis;
    }

    [HttpGet("redis-connection")]
    public async Task<ActionResult> TestRedisConnection()
    {
        var db = _redis.GetDatabase();
        
        // 测试连接和基本操作
        await db.StringSetAsync("test_key", $"Hello from {Environment.MachineName}");
        var value = await db.StringGetAsync("test_key");
        
        return Ok(new {
            server = Environment.MachineName,
            redisEndpoint = _redis.Configuration,
            testValue = value.ToString()
        });
    }
}

在三台服务器上分别调用测试

bash 复制代码
# 服务器A
curl http://server-a/api/test/redis-connection
# 输出: { "server": "ServerA", "testValue": "Hello from ServerA" }

# 服务器B  
curl http://server-b/api/test/redis-connection
# 输出: { "server": "ServerB", "testValue": "Hello from ServerB" }

# 服务器C
curl http://server-c/api/test/redis-connection  
# 输出: { "server": "ServerC", "testValue": "Hello from ServerC" }

关键观察:三台服务器操作的是同一个Redis键,后执行的会覆盖前面的值,证明它们连接的是同一个Redis实例。

6. 云服务方案

Azure Redis

csharp 复制代码
// 连接字符串格式
"your_redis_name.redis.cache.windows.net:6380,password=your_access_key,ssl=True,abortConnect=False"

AWS ElastiCache

csharp 复制代码
// 连接字符串格式  
"your-cluster-name.xxxxxx.clustercfg.use1.cache.amazonaws.com:6379"

7. 网络和安全考虑

防火墙配置

bash 复制代码
# 在Redis服务器上开放端口
ufw allow from 192.168.1.0/24 to any port 6379

连接验证脚本

csharp 复制代码
public async Task<bool> ValidateDistributedLock()
{
    var lockKey = "distributed_lock_test";
    var db = _redis.GetDatabase();
    
    // 模拟三个服务器同时获取锁
    var tasks = new List<Task<bool>>();
    
    for (int i = 0; i < 3; i++)
    {
        tasks.Add(Task.Run(async () =>
        {
            return await db.StringSetAsync(lockKey, Guid.NewGuid().ToString(), TimeSpan.FromSeconds(5), When.NotExists);
        }));
    }
    
    var results = await Task.WhenAll(tasks);
    
    // 应该只有一个服务器能获取到锁
    var successCount = results.Count(r => r);
    return successCount == 1;
}

总结

核心要点:

  1. Redis独立部署:在一台或多台专用服务器上部署Redis
  2. 应用服务器连接:所有应用服务器配置连接到同一个Redis实例
  3. 中心化协调:Redis作为中心化的状态存储和协调者
  4. 网络可达:确保所有应用服务器都能访问Redis服务器

这样部署后,三台服务器上的代码虽然相同,但通过连接同一个Redis实例,就能实现分布式协调,确保编码生成的唯一性。

架构选择

通过ds的回答,博主已经大概能Get到点了,博主基于对技术点的理解,发表博主自己的看法

好的,明白了,如果是基于这个方案实现,那么我是否可以理解为,我可以单独部署一台服务器,专门用来统一获取编码,然后让三台服务器每次获取编码时都通过api统一调用这个独立编码服务器

完全正确! 您提出的这个方案是一个很好的架构选择,这就是典型的"编码服务"或"序列服务"模式。

两种方案对比

方案1:Redis分布式锁(刚才讨论的)

复制代码
服务器A → Redis锁 → 数据库
服务器B → Redis锁 → 数据库
服务器C → Redis锁 → 数据库

方案2:独立编码服务(您提出的)

复制代码
服务器A → HTTP API → 独立编码服务 → 数据库
服务器B → HTTP API → 独立编码服务 → 数据库  
服务器C → HTTP API → 独立编码服务 → 数据库

独立编码服务实现

1. 编码服务API

csharp 复制代码
[ApiController]
[Route("api/[controller]")]
public class CodeServiceController : ControllerBase
{
    private readonly SemaphoreSlim _localLock = new SemaphoreSlim(1, 1);
    private readonly ApplicationDbContext _context;

    public CodeServiceController(ApplicationDbContext context)
    {
        _context = context;
    }

    [HttpPost("generate")]
    public async Task<ActionResult<CodeResponse>> GenerateCode([FromBody] CodeRequest request)
    {
        await _localLock.WaitAsync();
        try
        {
            // 生成编码的逻辑
            var code = await GenerateCodeInternalAsync(request.Prefix);
            
            return new CodeResponse
            {
                Code = code,
                GeneratedAt = DateTime.UtcNow,
                RequestId = Guid.NewGuid()
            };
        }
        finally
        {
            _localLock.Release();
        }
    }

    [HttpPost("batch")]
    public async Task<ActionResult<BatchCodeResponse>> GenerateBatchCodes([FromBody] BatchCodeRequest request)
    {
        await _localLock.WaitAsync();
        try
        {
            var codes = new List<string>();
            for (int i = 0; i < request.Count; i++)
            {
                codes.Add(await GenerateCodeInternalAsync(request.Prefix));
            }

            return new BatchCodeResponse { Codes = codes };
        }
        finally
        {
            _localLock.Release();
        }
    }

    private async Task<string> GenerateCodeInternalAsync(string prefix = "CODE")
    {
        // 查询当前最大编码
        var maxCode = await _context.Codes
            .Where(c => c.Code.StartsWith(prefix))
            .OrderByDescending(c => c.Id)
            .Select(c => c.Code)
            .FirstOrDefaultAsync();

        // 生成新编码
        var newCode = GenerateNextCode(maxCode, prefix);

        // 保存到数据库
        var codeEntity = new Code { Code = newCode, CreatedAt = DateTime.UtcNow };
        _context.Codes.Add(codeEntity);
        await _context.SaveChangesAsync();

        return newCode;
    }

    private string GenerateNextCode(string currentMaxCode, string prefix)
    {
        if (string.IsNullOrEmpty(currentMaxCode))
            return $"{prefix}000001";

        var numberPart = currentMaxCode.Substring(prefix.Length);
        if (int.TryParse(numberPart, out int number))
        {
            return $"{prefix}{(number + 1).ToString("D6")}";
        }

        return $"{prefix}000001";
    }
}

// DTOs
public class CodeRequest
{
    public string Prefix { get; set; } = "CODE";
}

public class CodeResponse
{
    public string Code { get; set; }
    public DateTime GeneratedAt { get; set; }
    public Guid RequestId { get; set; }
}

public class BatchCodeRequest
{
    public string Prefix { get; set; } = "CODE";
    public int Count { get; set; } = 1;
}

public class BatchCodeResponse
{
    public List<string> Codes { get; set; }
}

2. 应用服务器调用编码服务

csharp 复制代码
public class CodeServiceClient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<CodeServiceClient> _logger;

    public CodeServiceClient(HttpClient httpClient, ILogger<CodeServiceClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<string> GenerateCodeAsync(string prefix = "CODE")
    {
        try
        {
            var request = new CodeRequest { Prefix = prefix };
            var response = await _httpClient.PostAsJsonAsync("/api/codeservice/generate", request);
            
            if (response.IsSuccessStatusCode)
            {
                var result = await response.Content.ReadFromJsonAsync<CodeResponse>();
                _logger.LogInformation("成功获取编码: {Code}", result.Code);
                return result.Code;
            }
            else
            {
                throw new Exception($"编码服务返回错误: {response.StatusCode}");
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "调用编码服务失败");
            throw;
        }
    }

    public async Task<List<string>> GenerateBatchCodesAsync(int count, string prefix = "CODE")
    {
        var request = new BatchCodeRequest { Prefix = prefix, Count = count };
        var response = await _httpClient.PostAsJsonAsync("/api/codeservice/batch", request);
        
        response.EnsureSuccessStatusCode();
        var result = await response.Content.ReadFromJsonAsync<BatchCodeResponse>();
        return result.Codes;
    }
}

// 注册HttpClient
builder.Services.AddHttpClient<CodeServiceClient>(client =>
{
    client.BaseAddress = new Uri("https://code-service.yourcompany.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
});

3. 在业务代码中使用

csharp 复制代码
public class OrderService
{
    private readonly CodeServiceClient _codeClient;

    public OrderService(CodeServiceClient codeClient)
    {
        _codeClient = codeClient;
    }

    public async Task<Order> CreateOrderAsync(OrderCreateDto dto)
    {
        // 获取订单编号
        var orderCode = await _codeClient.GenerateCodeAsync("ORDER");
        
        var order = new Order
        {
            OrderCode = orderCode,
            CustomerName = dto.CustomerName,
            Amount = dto.Amount,
            CreatedAt = DateTime.UtcNow
        };

        // 保存订单逻辑...
        return order;
    }
}

两种方案的优缺点

Redis分布式锁方案

优点:

  • 性能较高(直接内存操作)
  • 延迟较低
  • 资源利用率高

缺点:

  • 需要维护Redis高可用
  • 锁逻辑分散在各个服务中
  • 业务复杂度较高

独立编码服务方案

优点:

  • 架构清晰:编码生成逻辑集中管理
  • 易于维护:只需要维护一个服务
  • 功能丰富:可以轻松添加批量生成、编码规则管理等功能
  • 客户端简单:应用服务器只需要调用HTTP API

缺点:

  • 单点故障风险(可通过集群解决)
  • 网络调用开销
  • 需要额外的服务器资源

高可用部署方案

编码服务集群

csharp 复制代码
// 使用负载均衡器
// 编码服务集群: code-service-1, code-service-2, code-service-3
// 负载均衡器: https://code-service.lb.com/

// 所有编码服务连接同一个数据库
// 使用数据库行级锁或乐观锁确保唯一性

带重试机制的客户端

csharp 复制代码
public class ResilientCodeServiceClient
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;

    public ResilientCodeServiceClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
        
        _retryPolicy = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(x => !x.IsSuccessStatusCode)
            .WaitAndRetryAsync(3, retryAttempt => 
                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
    }

    public async Task<string> GenerateCodeAsync(string prefix = "CODE")
    {
        var client = _httpClientFactory.CreateClient("CodeService");
        
        return await _retryPolicy.ExecuteAsync(async () =>
        {
            var request = new CodeRequest { Prefix = prefix };
            var response = await client.PostAsJsonAsync("/api/codeservice/generate", request);
            response.EnsureSuccessStatusCode();
            
            var result = await response.Content.ReadFromJsonAsync<CodeResponse>();
            return result.Code;
        });
    }
}

结论

您的方案是完全可行的,而且在很多场景下是更好的选择:

  1. 架构更清晰:关注点分离,编码生成逻辑集中管理
  2. 更易扩展:可以独立扩展编码服务
  3. 功能更丰富:便于添加编码规则、前缀管理、批量生成等功能
  4. 客户端更简单:应用服务器不需要关心并发控制

对于10个并发查询的场景,独立编码服务方案完全可以胜任,而且为未来的扩展提供了更好的基础。

架构图

通过架构图,辅助加深理解。

好的!我来为您绘制编码服务器和API服务器的架构图:

架构图

数据层 编码服务层 编码服务器集群 API服务器集群 服务器 A 服务器 B 负载均衡层 客户端层 数据库
SQL Server/MySQL
codes表 Redis缓存
可选 内部负载均衡器 编码服务 1
.NET Core 6.0 编码服务 2
.NET Core 6.0 编码服务 3
.NET Core 6.0 服务器 C API服务
.NET Core 6.0 API服务
.NET Core 6.0 负载均衡器
Nginx/HAProxy/ALB API服务
.NET Core 6.0 移动App 第三方系统

详细组件说明

1. 客户端层

复制代码
┌─────────────────┐
│   客户端层        │
├─────────────────┤
│ • Web浏览器      │
│ • 移动App        │
│ • 第三方系统     │
└─────────────────┘

2. 负载均衡层

yaml 复制代码
负载均衡器:
  类型: Nginx/HAProxy/AWS ALB
  功能:
    - SSL终止
    - 请求分发
    - 健康检查
    - 限流

3. API服务器集群

csharp 复制代码
// 每台API服务器配置
public class ApiServer
{
    // 业务逻辑处理
    public async Task<Order> CreateOrderAsync()
    {
        // 调用编码服务获取唯一编码
        var orderCode = await _codeClient.GenerateCodeAsync("ORDER");
        // 其他业务逻辑...
    }
}

4. 编码服务层

csharp 复制代码
// 编码服务器配置
public class CodeService
{
    // 统一的编码生成入口
    [HttpPost("generate")]
    public async Task<string> GenerateCodeAsync(string prefix)
    {
        // 使用本地锁确保单服务器内线程安全
        // 数据库唯一约束确保多服务器间数据安全
        return await GenerateUniqueCode(prefix);
    }
}

5. 数据层

sql 复制代码
-- 编码表结构
CREATE TABLE codes (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    code VARCHAR(50) NOT NULL UNIQUE,
    prefix VARCHAR(20) NOT NULL,
    created_at DATETIME NOT NULL,
    business_type VARCHAR(50)
);

-- 唯一索引确保编码唯一性
CREATE UNIQUE INDEX idx_code_unique ON codes(code);

请求流程图

客户端 负载均衡器 API服务器 编码负载均衡器 编码服务 数据库 POST /api/orders 转发请求到服务器A POST /api/codes/generate 转发到编码服务1 获取本地锁 查询当前最大编码 插入新编码 释放本地锁 返回新编码 CODE2023110001 创建订单记录 返回订单结果 订单创建成功 客户端 负载均衡器 API服务器 编码负载均衡器 编码服务 数据库

部署配置示例

API服务器配置

json 复制代码
{
  "CodeService": {
    "BaseUrl": "http://code-service-internal-lb/",
    "Timeout": 30,
    "RetryCount": 3
  },
  "Database": {
    "ConnectionString": "Server=db-server;Database=app;User=user;Password=pass;"
  }
}

编码服务器配置

json 复制代码
{
  "Database": {
    "ConnectionString": "Server=db-server;Database=codes;User=user;Password=pass;"
  },
  "Redis": {
    "ConnectionString": "redis-server:6379,abortConnect=false"
  }
}

这种架构确保了:

  1. 高可用:每层都有冗余
  2. 可扩展:可以水平扩展API服务器和编码服务器
  3. 职责分离:编码生成逻辑集中管理
  4. 数据一致性:通过数据库唯一约束保证编码唯一性

总结

通过上面了解,那么还可以继续想一想。

如果上千上万并发的情况下,那么独立编码服务器也支撑不下去的时候怎么?

博主的想法是,划分编码服务器。

相关推荐
weixin_537765802 小时前
【负载均衡】LVS原理与配置
服务器·负载均衡·lvs
我什么都学不会2 小时前
DNS主从服务器练习
linux·运维·服务器
zt1985q2 小时前
外网访问项目研发管理软件 codes
运维·服务器·windows·网络协议·tcp/ip
华纳云IDC服务商2 小时前
DNS记录更新后为什么还是访问不到新服务器?
运维·服务器
Charles_go2 小时前
C#7、如何处理异常
开发语言·c#
居7然2 小时前
详解监督微调(SFT):大模型指令遵循能力的核心构建方案
人工智能·分布式·架构·大模型·transformer
涛声依旧393163 小时前
安装部署自己的nginx
运维·服务器·nginx
wanhengidc3 小时前
物理服务器都有哪些作用?
运维·服务器·安全·智能手机·云计算
milanyangbo3 小时前
从局部性原理到一致性模型:深入剖析缓存设计的核心权衡
开发语言·后端·缓存·架构