19

RedLock 实现分布式锁

 5 years ago
source link: http://beckjin.com/2019/01/06/redLock-net/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

并发是程序开发中不可避免的问题,根据系统面向用户、功能场景的不同,并发的重视程度会有不同。从程序的角度来说,并发意味着相同的时间点执行了相同的代码,而有些情况是不被允许的,比如:转账、抢购占库存等,如果没有做好临界条件的验证,会带来非常严重的后果。追根结底是因为并发引起的数据不一致问题,面对并发,我们通常会采用锁来优化。

场景模拟

如下模拟抢购的示例代码(C#):

// 有10个商品库存
private static int stockCount = 10;

public bool Buy()
{
	// 模拟执行的逻辑代码花费的时间
	Thread.Sleep(new Random().Next(100,500));
	if (stockCount > 0)
	{
		stockCount--;
		return true;
	}
	return false;
}
var test = new Test();

Parallel.For(1, 16, (i) =>
{
	var stopwatch = new Stopwatch();
	stopwatch.Start();
	var data = test.Buy();
	stopwatch.Stop();
	Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId}, Result:{data}, Time:{stopwatch.ElapsedMilliseconds}");
});
Console.ReadKey();

模拟并行调用 Buy 方法 15 次(内部使用的是线程池,所以 ThreadId 会有重复),实际上只有 10 个库存,返回结果却显示 11 个请求都购买成功了。

ZjiUfeZ.png!web

单机部署模式解决方案

在单机部署模式下,我们只需要加 lock(){} 就可以解决问题:

// 有10个商品库存
private static int stockCount = 10;

private static object obj = new object();

public bool Buy()
{
	lock (obj)
	{
		// 模拟执行的逻辑代码花费的时间
		Thread.Sleep(new Random().Next(100, 500));
		if (stockCount > 0)
		{
			stockCount--;
			return true;
		}
		return false;
	}
}

yiemAn2.png!web

从输出结果中可以看出,确实只有10个请求是显示购买成功,但同时发现部分请求的执行时间明显变长,这就是加锁带来的最直观影响,当某个线程获得锁之后,在没有释放之前,其他线程只能继续等待,并发越高,更多的线程需要等待轮流被处理。

各种语言一般都提供了锁的实现,用法大同小异,语言本身实现的锁只能作用于当前进程内,所以在单机模式部署的系统中使用基本没什么问题。

集群部署模式解决方案(分布式锁)

在集群模式下,系统部署于多台机器(一个系统运行在多个进程中),语言本身实现的锁只能确保当前进程内有效(基于内存),多进程就没办法共享锁状态,这时我们就得考虑采用分布式锁,分布式锁可以采用 数据库ZooKeeperRedis 等来实现,最终都是为了达到在不同的进程、线程内能共享锁状态的目的。

这里将介绍基于 Redis 的 RedLock.net 来解决分布式下的并发问题,RedLock.net 是 RedLock 分布式锁算法的 .NET 版实现 ( 大部分语言都有对应的实现, 查看 ) ,RedLock 分布式锁算法是由 Redis 的作者提出。

RedLock 简介

RedLock 的思想是使用多台 Redis Master ,节点完全独立,节点间不需要进行数据同步,因为 Master-Slave 架构一旦 Master 发生故障时数据没有复制到 Slave,被选为 Master 的 Slave 就丢掉了锁,另一个客户端就可以再次拿到锁。锁通过 setNX(原子操作) 命令设置,在有效时间内当获得锁的数量大于 (n/2+1) 代表成功,失败后需要向所有节点发送释放锁的消息。

获取锁:

SET resource_name my_random_value NX PX 30000

释放锁:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

RedLock.net 集成

  1. 创建 .NETCore API 项目

  2. Nuget 安装 RedLock.net

    Install-Package RedLock.net
    
  3. appsettings.json 添加 redis 配置

    {
      "RedisUrl": "127.0.0.1:6379", // 多个用,分割
      ...
    }
    
  4. 添加 ProductService.cs,模拟商品购买

    // 有10个商品库存,如果同时启动多个API服务进行测试,这里改成存数据库或其他方式
    private static int stockCount = 10;
    public async Task<bool> BuyAsync()
    {
    	// 模拟执行的逻辑代码花费的时间
    	await Task.Delay(new Random().Next(100, 500));
    	if (stockCount > 0)
    	{
    		stockCount--;
    		return true;
    	}
    	return false;
    }
    
  5. 修改 Startup.cs ,创建 RedLockFactory

    定义 RedLockFactory 变量:

    private RedLockFactory lockFactory;
    

    添加方法:

    private RedLockFactory GetRedLockFactory()
    {
    	var redisUrl = Configuration["RedisUrl"];
    	if (string.IsNullOrEmpty(redisUrl))
    	{
    		throw new ArgumentException("RedisUrl 不能为空");
    	}
    	var urls = redisUrl.Split(",").ToList();
    	var endPoints = new List<RedLockEndPoint>();
    	foreach (var item in urls)
    	{
    		var arr = item.Split(":");
    		endPoints.Add(new DnsEndPoint(arr[0], Convert.ToInt32(arr[1])));
    	}
    	return RedLockFactory.Create(endPoints);
    }
    

    在 ConfigureServices 注入 IDistributedLockFactory:

    lockFactory = GetRedLockFactory();
    services.AddSingleton(typeof(IDistributedLockFactory), lockFactory);
    services.AddScoped(typeof(ProductService));
    

    修改 Configure,应用程序结束时释放 lockFactory :

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime)
    {
    	...
    
    	lifetime.ApplicationStopping.Register(() =>
    	{
    		lockFactory.Dispose();
    	});
    }
    
  6. 在 Controller 添加方法 DistributedLockTest

    private readonly IDistributedLockFactory _distributedLockFactory;
    private readonly ProductService _productService;
    
    public HomeController(IDistributedLockFactory distributedLockFactory,
    	ProductService productService)
    {
    	_distributedLockFactory = distributedLockFactory;
    	_productService = productService;
    }
    
    [HttpGet]
    public async Task<bool> DistributedLockTest()
    {
    	var productId = "id";
    	// resource 锁定的对象
    	// expiryTime 锁定过期时间,锁区域内的逻辑执行如果超过过期时间,锁将被释放
    	// waitTime 等待时间,相同的 resource 如果当前的锁被其他线程占用,最多等待时间
    	// retryTime 等待时间内,多久尝试获取一次
    	using (var redLock = await _distributedLockFactory.CreateLockAsync(productId, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(20)))
    	{
    		if (redLock.IsAcquired)
    		{
    			var result = await _productService.BuyAsync();
    			return result;
    		}
    		else
    		{
    			Console.WriteLine($"获取锁失败:{DateTime.Now}");
    		}
    	}
    	return false;
    }
    
  7. 调用接口测试

       Parallel.For(1, 16, (i) =>
    {
    	var stopwatch = new Stopwatch();
    	stopwatch.Start();
    	var data = GetAsync($"http://localhost:5000/home/distributedLockTest").Result;
    	stopwatch.Stop();
    	Console.WriteLine($"ThreadId:{Thread.CurrentThread.ManagedThreadId}, Result:{data}, Time:{stopwatch.ElapsedMilliseconds}");
    });
    

    jYfIFfM.png!web

关于 RedLock 分布式锁算法的争议大家可以参考:

How to do distributed locking

Is Redlock safe?

总结

如果使用锁,必然对性能上会有一定影响,我们需要根据实际场景来判断是真正需要。在指定锁过期时间时要相对合理,避免出现锁已过期,但逻辑还没执行完成,这样就失去了锁的意义,当然这种情况下我们还可以考虑重入锁。

最后推荐一下微软开源的一个基于 Actor 模型的分布式框架 Orleans ,也可以达到分布式锁的效果。

参考链接


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK