.net core 用redis实现分布式锁
维基百科定义
分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
分布式锁实现目的
针对共享资源要求串行化处理,才能保证安全且合理的操作。
例:秒杀场景,并发量很高的情况下,同一个商品只能被一个用户抢到。
实现步骤
- 测试思路:我准备新建一张表,里面一个字段num,提供一个webapi接口用redis实现分布式锁,接口对num进行加500次,然后使用jmeter,同时调用接口500次(模拟500并发),最后结果等于500才是业务正确情况。
1.创建数据库
数据库我使用sqlserver,执行以下语句:
CREATE DATABASE RedisLockdDemo;
USE RedisLockdDemo;
CREATE TABLE RedisLockdTest(
Num INT NOT NULL
);
INSERT INTO RedisLockdTest VALUES(0);
2.编写未实现分布式锁接口代码
我这里使用.net 5 开发webapi接口,先用没实现分布式锁的代码来运行看下结果如何。
- ps:我这里没有搭建分布式环境,但是有并发同样可以模拟出效果,正确测试方法应该是将该接口发布多份然后用nginx等工具进行反向代理再进行测试。
部分代码如下:
3.然后使用jmeter 同时调用接口500次,模拟500并发
启动,调用完毕后,查询一下数据库结果,Num值是388,很明显因为并发执行导致结果和预期不一致。
4.接下来改造代码,加入分布式锁
编写分布式锁实现类 RedisLock.cs
我这里操作Redis,使用 CSRedisCore
using CSRedis;
using System.Threading;
using System.Threading.Tasks;
namespace RedisLock_Demo
{
public class RedisLock
{
/// <summary>
/// 锁key
/// </summary>
private readonly string lockKey = "RedisLock";
/// <summary>
/// 锁的过期秒数
/// </summary>
private readonly int lockTime = 20;
/// <summary>
/// 续命线程取消令牌
/// </summary>
private CancellationTokenSource tokenSource = new CancellationTokenSource();
public RedisLock()
{
RedisHelper.Initialization(new CSRedis.CSRedisClient("127.0.0.1:6379,password=123456,poolsize=50,ssl=false,writeBuffer=10240"));
}
/// <summary>
/// 获取锁
/// </summary>
/// <param name="requestId">请求id保证释放锁时的客户端和加锁的客户端一致</param>
/// <returns></returns>
public bool GetLock(string requestId)
{
//设置key 设置过期时间20s
while (true)
{
//设置key Redis2.6.12以上版本,可以用set获取锁。set可以实现setnx和expire,这个是原子操作
if (RedisHelper.Set(lockKey, requestId, lockTime, RedisExistence.Nx))
{
//设置成功后开启子线程为key续命
CreateThredXm();
return true;
}
}
}
/// <summary>
/// 为锁续命(防止业务操作时间大于锁自动释放时间,锁被自动释放掉)
/// </summary>
void CreateThredXm()
{
Task.Run(() =>
{
while (true)
{
Thread.Sleep(10);
//外部取消 退出子线程
if (tokenSource.IsCancellationRequested)
{
return;
}
//查询key还有多少秒释放
var Seconds = RedisHelper.PTtl(lockKey) / 1000;
//key还剩1/3秒时重设过期时间
if (Seconds < (lockTime / 3))
{
//小于5秒则自动 重设过期时间
RedisHelper.Expire(lockKey, lockTime);
}
}
}, tokenSource.Token);
}
/// <summary>
/// 释放锁操作
/// </summary>
/// <param name="requestId">请求id保证释放锁时的客户端和加锁的客户端一致</param>
public void ReleaseLock(string requestId)
{
//这里使用Lua脚本保证原子性操作
string script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
RedisHelper.Eval(script, lockKey, requestId);
//取消续命线程
tokenSource.Cancel();
}
}
}
5.然后改造控制器业务代码
using Microsoft.AspNetCore.Mvc;
using RedisLock_Demo.Models;
using System;
namespace RedisLock_Demo.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class RedisLockController : ControllerBase
{
readonly IFreeSql freeSql;
readonly RedisLock redisLock = new RedisLock();
public RedisLockController(IFreeSql _freeSql)
{
freeSql = _freeSql;
}
[HttpPost]
public int Post()
{
//请求id
string requestId = Guid.NewGuid().ToString();
try
{
//获取锁
redisLock.GetLock(requestId);
//业务操作
var redisLockd = freeSql.Select<RedisLockdTest>().First();
redisLockd.Num++;
freeSql.Update<RedisLockdTest>().Set(r => r.Num, redisLockd.Num).Where(r => 1 == 1).ExecuteAffrows();
return redisLockd.Num;
}
finally
{
redisLock.ReleaseLock(requestId);
}
}
}
}
6.再使用jmeter 同时调用接口500次,模拟500并发
调用完毕后再查询数据库结果,Num已经是500,就正确实现了业务需求了。
总结
本文主要介绍了如何使用.net 实现redis分布式锁,不过本文是基于单机部署redis实现的分布式锁,如果采用集群部署则可以参考官方提出的方式。