第一篇我们总结了秒杀的整个流程,本篇我们详细介绍下redis的秒杀实现,基于.net core2.2开发。
首先,需要安装redis,因为我在本地测试的,所以安装的windows版本的redis。redis分为服务端和客户端,这个redis怎么安装,本篇不详细说明,如果有安装问题和无法下载redis的windows版本的话可以私聊我。
第二步,就是编码部分,新建一个web api接口服务,使用redis的lua脚本做库存扣减,属于原子操作。大家就用下面的代码,可以支持1000个先线程的并发,在大的并发我没有测试,可能需要换架构。核心编码如下:
public string doKill(string threadId)
{
string tId = threadId;
string uid = getCheckNumber(); //生成一个随机的用户id
string prodid = "1";//商品固定001
try
{
ConnectionMultiplexer redis = radisTemplate.getConn();
IDatabase db = redis.GetDatabase();
//定义Lua脚本
string secKillScript = "local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='sk:'..prodid..\":qt\";\r\n" +
"local usersKey='sk:'..prodid..\":user\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1";
RedisKey[] s = new RedisKey[2];
s[0] = uid;
s[1] = prodid;
//使用ScriptEvaluate执行脚本
Object result = db.ScriptEvaluate(secKillScript, s);
string result1 = result.ToString();
if ("0".Equals(result1))
{
Log.Info($"线程:{tId}:已抢空");
return "已抢空!!";
}
else if ("1".Equals(result1))
{
RequestDto requestDto = new RequestDto
{
Id = tId,
SuccessDate = DateTime.Now,
Message = $"请求成功:{tId}"
};
string str = JsonConvert.SerializeObject(requestDto);
db.ListLeftPushAsync("sk2", str);
Log.Info($"线程:{tId}:抢到了");
return "抢购成功!!!!";
}
else if ("2".Equals(result1))
{
Log.Info($"线程:{tId}:该用户已抢过");
return "该用户已抢过!!";
}
else
{
Log.Info($"线程:{tId}:抢购异常");
return "抢购异常!!";
}
}
catch (Exception ex)
{
Log.Info($"线程:{tId}:发生错误");
Console.WriteLine(ex);
Log.Error($"发生错误:{ex}");
return "系统异常!!";
}
}
public static String getCheckNumber()
{
Random rd = new Random();
int num = rd.Next(100000, 1000000);
return num.ToString();
}
///这上面是接口代码
///这边封装的一个redis帮助类
public class RadisConn
{
//定义连接器
static ConnectionMultiplexer redis = null;
//获取连接数据库
public ConnectionMultiplexer getConn()
{
try
{
if (redis != null && redis.IsConnected)
{
//Log.Info($"redis实例有效,直接返回!");
return redis;
}
//Log.Info($"redis实例无效,开始连接服务!");
redis = ConnectionMultiplexer.Connect("127.0.0.1:6379,password=123456,abortConnect=false");
//if (redis.IsConnected)
//{
// Log.Info($"redis 连接服务成功!");
//}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
return redis;
}
}
public class RequestDto
{
public string Id { get; set; }
public DateTime SuccessDate { get; set; }
public string Message { get; set; }
}
第三步,切换到redis客户端,然后设置库存 set sk:1:qt 50
第四部,用压力测试工具或者自己写一个200个任务的请求这个接口,看看库存扣减是否正常。提供一个多线程实例,代码如下:
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace HttpClient请求
{
internal class Program
{
static void Main(string[] args)
{
try
{
for (int i = 0; i <40; i++)
{
Task task = Task.Run(() =>
{
Console.WriteLine($"创建线程成功,当前线程号:{Task.CurrentId}");
SendRequest(Task.CurrentId.ToString());
});
}
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
static void SendRequest(string threadId)
{
try
{
var url = $"https://localhost:5001/api/values/doKill?threadId="+ threadId;
using (var client = new HttpClient())
{
var content = client.GetStringAsync(url).Result;
//var data = JsonConvert.DeserializeObject<dynamic>(content);
Console.WriteLine($"线程号:{threadId}:{content}");
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}