NetCore3.1连接Redis做秒杀案例

测试环境:netcore3.1   redis-6.2.4


一:安装Redis

尽管在不是系统性介绍Radis的地方介绍安装radis并不是一件明智的事情,但本着能跑起来就算成功的原则,这里简单介绍一下。

1.1 首先去https://redis.io/下载对应的tar包,默认我们要在linux系统下安装,这里使用CentOS7做演示

NetCore3.1连接Redis做秒杀案例_第1张图片

1.2 安装必备的c语言编译环境

yum install centos-release-scl scl-utils-build && yum install -y devtoolset-8-toolchain && scl enable devtoolset-8 bash

centos8只需要装一下gcc即可

测试gcc版本

NetCore3.1连接Redis做秒杀案例_第2张图片

1.3 将下载好的redis-6.2.4.tar.gz放在/opt目录下

在这里插入图片描述
使用tar -zxvf redis-6.2.4.tar.gz解压文件,解压完成后cd进入redis-6.2.4目录

tar -zxvf redis-6.2.4.tar.gz && cd redis-6.2.4

在redis-6.2.4目录下执行make命令(编译)

(补充:这里如果没有安装c语言环境,make会报错—Jemalloc/jemalloc.h:没有那个文件,解决办法是运行make distclean,然后再重新make)

NetCore3.1连接Redis做秒杀案例_第3张图片
等到如上方图示一样出现了 It’s a good idea to run ‘make test’ 字样,证明编译成功,于是我们直接跳过测试,使用make install来完成最后的安装

NetCore3.1连接Redis做秒杀案例_第4张图片
安装目录:/usr/local/bin

在这里插入图片描述
查看默认安装目录:

redis-benchmark:性能测试工具,可以在自己本子运行,看看自己本子性能如何

redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲

redis-check-dump:修复有问题的dump.rdb文件

redis-sentinel:Redis集群使用

redis-server:Redis服务器启动命令

redis-cli:客户端,操作入口

1.4 后台启动radis,首先备份redis.conf文件,拷贝一份到其他目录

mkdir /myredis && cp  /opt/redis-6.2.4/redis.conf  /myredis/redis.conf

修改redis.conf(257行)文件将里面的daemonize no 改成 yes,让服务在后台启动

NetCore3.1连接Redis做秒杀案例_第5张图片
如果希望可以被远程访问,注释掉bind 127.0.0.1 的配置

如果想设置密码,搜索requirepass,设置格式为:

requirepass 123   指定密码123

然后使用redis-server /myredis/redis.conf启动服务

redis-server /myredis/redis.conf

在这里插入图片描述
用客户端访问redis-cli

在这里插入图片描述
使用Ping验证

在这里插入图片描述
如果设置了密码,登录进入后输入 auth [你的密码] 回车完成验证。

如果有需求,不要忘记端口放过

firewall-cmd --zone=public --add-port=6379/tcp --permanent    #开启指定端口
systemctl restart firewalld.service 	 				    #重启防火墙





二:测试在.net中连接redis

1.1 准备一个.net项目,我们先要在NuGet中下载对应的包 StackExchange.Redis
NetCore3.1连接Redis做秒杀案例_第6张图片

1.2 自建一个Controller,路由随意,我们准备往名为 "sk:001:qt"的key中添加一个数值50,用来作为我们的库存,其中001是商品的编号,代码如下:

       
    [ApiController]
    [Route("Val")]
    public class ValuesController : Controller
    {
       
       //添加商品库存
       [Route("add")]
        public string Add(string prodid) {
            if (prodid == null)
            {
                return "产品id不能为空";
            }
            //redis连接
            ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("192.168.132.136:6379,password=123123");
            //获取redis数据库
            IDatabase db = redis.GetDatabase();
            //拼接库存Key
            string stockKey = "sk:" + prodid + ":qt";
            //调用StringSet方法来设置kv
            db.StringSet(stockKey, 50);
            return "设置 " + stockKey + " 商品50件库存成功.";
        }
        
	}

访问接口:https://localhost:5001/Val/add?prodid=001
NetCore3.1连接Redis做秒杀案例_第7张图片
radis-cli中查看是不是有对应的数据

NetCore3.1连接Redis做秒杀案例_第8张图片
如此便说明连接测试成功。





三:做简单的秒杀案例

思路:

首先需要一个stockKey,也就是我们前面所提到的商品库存key,是一个String类型,用户通过指明该库存key来进行有选择的消费;接着需要一个userKey,是一个Set类型,维护着所有已经参与秒杀的成员列表,因为我们的秒杀是只允许用户参与一次,因此,只要用户出现在了这个userKey的集合中,就可以判定其已经参与过秒杀,从而不放行后面的操作。

步骤1:uid和pid非空判断,uid是用户编号,pid(prodid)是商品编号

步骤2:拼接key

步骤3:获取库存,如果库存为Null,说明秒杀还没开始

步骤4:判断用户是否重复秒杀

步骤5:判断商品数量,数量小于1结束秒杀

步骤6:执行秒杀

步骤7:把成功用户放清单里



代码:

       
    [ApiController]
    [Route("Val")]
    public class ValuesController : Controller
    {
       
        //执行秒杀测试
        [Route("kill")]
        public String Index(String uid,String prodid )
        {
   
            //redis连接
            ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("192.168.132.133:6379,password=123123");
            //获取redis数据库
            IDatabase db = redis.GetDatabase();

            //1 uid和pid非空判断
            if (uid == null || prodid == null)
            {
                return "id不能为空";
            }

            //2 拼接key
            //2.1 库存Key
            String stockKey = "sk:" + prodid + ":qt";
            //2.2 秒杀成功用户key
            String userKey = "sk:" + prodid + ":user";

            //3 获取库存,如果库存为Null,秒杀还没开始
            string stock = db.StringGet(stockKey);
            if (stock == null)
            {
                redis.Close();
                return "秒杀还没开始";
            }

            //4 判断用户是否重复秒杀
            if (db.SetContains(userKey, uid)) {
                redis.Close();
                return "您已经秒杀过商品,请勿重复操作。";
            }

            //5 判断商品数量,数量小于1结束秒杀
            if (int.Parse(stock) < 1) {
                redis.Close();
                return "秒杀活动已经结束";
            }

            //6 秒杀过程
            //6.1 库存减一
            db.StringDecrement(stockKey);

            //7.2 把成功用户放清单里
            db.SetAdd(userKey, uid);
            
            //关闭连接
            redis.Close();
            
            return "用户:" + userKey + " 秒杀成功!! 剩余库存为:"+db.StringGet(stockKey);
        }
        
	}

直接访问测试:https://localhost:5001/Val/kill?uid=1&prodid=001
在这里插入图片描述
第二次直接测试:https://localhost:5001/Val/kill?uid=1&prodid=001

NetCore3.1连接Redis做秒杀案例_第9张图片
查看radis-cli:
NetCore3.1连接Redis做秒杀案例_第10张图片
至于上述使用到的StringDecrement 等等方法是怎么来的,可以查看IDatabase的源码找到,这里无非就是在原始radis的操作上做一层封装,名称里大部分也都带着Set,String,Hash等字眼,仔细找找就可以发现自己需要的方法。

NetCore3.1连接Redis做秒杀案例_第11张图片
这里简单的对上面使用过的方法进行一个总结

方法 作用
StringGet(key) 获取指定字符串key下的value
SetContains(key,value) 判断key为key的Set集合中包不包含该value
StringDecrement(key) 对指定key下的value进行递减,默认步长为1
SetAdd(key,vlaue) 将制定的value添加到对应key下的集合中

这样一来,一个简陋的秒杀逻辑就完成了。





四:并发测试

但是这样真的安全吗?

这里我们稍微改动一下代码方便用来测试,显示添加了一个工具类用来随机生成用户id。

RandomNumUtil

    public class RandomNumUtil
    {
        //随机生成六位数的验证码
        public static String getCheckNumber()
        {
            Random rd = new Random();
            int num = rd.Next(100000, 1000000);
            return num.ToString();
        }
    }

然后是修改kill方法:

     [Route("kill")]
        public String Index()
        {
            //生成一个随机的用户id
            string uid = RandomNumUtil.getCheckNumber();
            string prodid = "001";//商品固定001

            //redis连接
            ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("124.71.96.193:6379,password=Project@2020");
            //获取redis数据库
            IDatabase db = redis.GetDatabase();

            //1 uid和pid非空判断
            if (uid == null || prodid == null)
            {
                return "id不能为空";
            }

            //2 拼接key
            //2.1 库存Key
            String stockKey = "sk:" + prodid + ":qt";
            //2.2 秒杀成功用户key
            String userKey = "sk:" + prodid + ":user";

            //3 获取库存,如果库存为Null,秒杀还没开始
            string stock = db.StringGet(stockKey);
            if (stock == null)
            {
                redis.Close();
                return "秒杀还没开始";
            }

            //4 判断用户是否重复秒杀
            if (db.SetContains(userKey, uid)) {
                redis.Close();
                return "您已经秒杀过商品,请勿重复操作。";
            }

            //5 判断商品数量,数量小于1结束秒杀
            if (int.Parse(stock) < 1) {
                redis.Close();
                return "秒杀活动已经结束";
            }

            //6 秒杀过程
            //6.1 库存减一
            db.StringDecrement(stockKey);

            //7.2 把成功用户放清单里
            db.SetAdd(userKey, uid);

            //关闭连接
            redis.Close();


            return  " 秒杀成功!! ";


        }

我们使用Jmeter压测一发,先温柔的来上50个线程
NetCore3.1连接Redis做秒杀案例_第12张图片

填写对应的测试信息
NetCore3.1连接Redis做秒杀案例_第13张图片Run一波,等上个六七秒钟Stop
NetCore3.1连接Redis做秒杀案例_第14张图片后台直接爆了异常,因为我们一直裸连太消耗性能了。
NetCore3.1连接Redis做秒杀案例_第15张图片接着去看看Radis战况如何,发现商品直接被卖成负数了。
在这里插入图片描述出现这种情况的原因,是因为我们的扣减库存的操作根本就不是原子级的,在高并发的情况下,线程间的可见性不是很好,这就导致线程彼此都对同一个操作进行了提交,从而卖成了负数。





五:结合Lua脚本解决库存问题

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。因此,这里我们就可以通过Lua来调一些底层的钩子,从而完成一些原子级的操作。

在此之前,我们先对radis连接行为进行一个简单的封装,然后让它自己注入进去,只不过这里我封装的非常简陋,按理说是应该将所有用到的方法都封装一下,只不过为测试,且希望直接掉它的Close方法,因此只是对ConnectionMultiplexer进行了一个封装。


RadisConn

    //获取Redis连接工具
    public class RadisConn
    {
        //定义连接器
        static ConnectionMultiplexer redis = null;

        //获取连接数据库
        public  ConnectionMultiplexer getConn() {
            try
            {
                redis = ConnectionMultiplexer.Connect("192.168.132.123:6379,password=123123");
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            
            return redis;
        } 

    }

Startup.cs

  public class Startup
    {


        public void ConfigureServices(IServiceCollection services)
        {
            //支持mvc服务
            services.AddControllersWithViews();
            //DI依赖注入
            services.AddSingleton<RadisConn>();

        }
       
    }

ValuesController

    [ApiController]
    [Route("Val")]
    public class ValuesController : Controller
    {
        //radis数据库连接
        private RadisConn radisTemplate;

        public ValuesController(RadisConn radisTemplate)
        {   //依赖注入
            this.radisTemplate = radisTemplate;
        }
}

5.1 首先是在NuGet中下载NLua依赖包
NetCore3.1连接Redis做秒杀案例_第16张图片
5.2 ValueController中改进方法


        [Route("do")]
        public string doKill() {
           
            string uid = RandomNumUtil.getCheckNumber(); //生成一个随机的用户id
            string prodid = "001";//商品固定001

            //redis连接
            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();
            //关闭现有连接
            redis.Close();
            if ("0".Equals(result1))
            {
                return "已抢空!!";
            }
            else if ("1".Equals(result1))
            {
                return  "抢购成功!!!!";
            }
            else if ("2".Equals(result1))
            {
                return  "该用户已抢过!!";
            }
            else
            {
                return  "抢购异常!!";
            }
        }


如此一来,借由lua提供的原子级操作,在使用Jmeter压测的时候,就不会出现库存为负数的情况了。

完整脚本内容如下:

local userid=KEYS[1]; 
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":user'; 
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then 
  return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then 
  return 0; 
else 
  redis.call("decr",qtkey);
  redis.call("sadd",usersKey,userid);
end
return 1;

你可能感兴趣的:(.netcore,.netcore,redis,lua)