前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >保姆级Redis秒杀解决方案设计(lua脚本解读)

保姆级Redis秒杀解决方案设计(lua脚本解读)

作者头像
冷环渊
发布2021-10-19 15:55:45
2.1K0
发布2021-10-19 15:55:45
举报

redis

秒杀案例

在这里插入图片描述
在这里插入图片描述

以上为例 我们创建一个项目 Springbooy : serkill

问题思考 秒杀要解决什么问题 1.超卖 2.连接超时 3.库存遗留 编写秒杀过程:doseckill’方法

代码语言:javascript
复制
	public  boolean doSecKill(String uid,String prodid)
	{

		Jedis jedis = new Jedis("120.79.14.203",6379);
		jedis.auth("123456");
		//1:uid和proid的非空判断
		if (uid==null||prodid==null){
			return false;
		}
		System.out.println(uid);
		System.out.println(prodid);
		//3.1库存key
		String kckey = "sk"+prodid+"qt";

		//3.2秒杀成功用户key
		String userkey = "sk"+prodid+"user";
		//4 获取库存本身等于空,秒杀还没有开始
		jedis.watch(kckey);
		System.out.println(kckey);
		String s = jedis.get(kckey);
		if (s==null){
			System.out.println("秒杀还没有开始,请等待");

			return false;
		}
		//5.用户是否重复秒杀操作
		Boolean member = jedis.sismember(userkey, uid);
		if (member){
			System.out.println("你已经秒杀过了不要再次重复的秒杀");

			return false;
		}
		//6.秒杀的过程
		if (Integer.parseInt(s)<=0){
			System.out.println("秒杀已经结束了");

			return false;
		}
		//7秒杀过程
		Transaction multi = jedis.multi();
		//7.1库存-1
		multi.decr(kckey);

		//7.2把秒杀成功的用户添加到redis
		multi.sadd(userkey,uid);
		List exec = multi.exec();
		System.out.println(exec);
		if (exec==null || exec.size()==0){
			System.out.println("秒杀失败了");
			return false;
		}
		System.out.println("秒杀成功");
		return true;
	}

前端写一个简单的表单

在这里插入图片描述
在这里插入图片描述

之后使用阿帕奇的jmeter来测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

并发测试之后 会发现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

有库存遗留,并没有卖完,这里并发的并发问题可以用脚本语言 : lua来解决

简单介绍一下

LUA脚本在Redis中的优势

  • 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
  • LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
  • 但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。 利用lua脚本淘汰用户,解决超卖问题.
  • redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。解决例如 2000用户秒杀 800库存 却还剩下600 并发问题

lua脚本业务类编写

代码语言:javascript
复制
package com.hyc.serkill.config;

import org.slf4j.LoggerFactory;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

public class SecKill_redisByScript {

	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;

	public static void main(String[] args) {
		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();

		Jedis jedis=jedispool.getResource();
		System.out.println(jedis.ping());

		Set<HostAndPort> set=new HashSet<HostAndPort>();

	//	doSecKill("201","sk:0101");
	}

	static 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..\":usr\"\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" ;

	static String secKillScript2 =
			"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
			" return 1";
}

脚本代码解读

大致来为大家读一下这个脚本代码的意思哈,我本人也没有学过lua但是看是可以看懂一些的

代码语言:javascript
复制
			//获得参数1
            "local userid=KEYS[1];\r\n" +  
            //获得参数2
			"local prodid=KEYS[2];\r\n" +
			//生成秒杀库存key
			"local qtkey='sk'..prodid..\"qt\";\r\n" +
			//生成秒杀库存key
			"local usersKey='sk'..prodid..\":usr\"\r\n" +
			//判断redis查找set集合中userid的数字
			"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
			//如果返回是1那么表示已经秒杀过了,retrun2:代表抢购过了,方便后续调用判断
			"if tonumber(userExists)==1 then \r\n" +
			"   return 2;\r\n" +
			"end\r\n" +
			//获取库存
			"local num= redis.call(\"get\" ,qtkey);\r\n" +
			//判断如果小于等于0那么返回0 表示已经没有了
			"if tonumber(num)<=0 then \r\n" +
			"   return 0;\r\n" +
			//要是不等于0执行库存减少操作,将用户的id存入道用户key中,返回1 代表秒杀成功
			"else \r\n" +
			"   redis.call(\"decr\",qtkey);\r\n" +
			"   redis.call(\"sadd\",usersKey,userid);\r\n" +
			"end\r\n" +
			"return 1" ;

之后在下面编写方法

代码语言:javascript
复制
	public static boolean doSecKill(String uid,String prodid) throws IOException {

		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis=jedispool.getResource();

		 //String sha1=  .secKillScript;
		String sha1=  jedis.scriptLoad(secKillScript);
		Object result= jedis.evalsha(sha1, 2, uid,prodid);

		  String reString=String.valueOf(result);
		if ("0".equals( reString )  ) {
			System.err.println("已抢空!!");
		}else if("1".equals( reString )  )  {
			System.out.println("抢购成功!!!!");
		}else if("2".equals( reString )  )  {
			System.err.println("该用户已抢过!!");
		}else{
			System.err.println("抢购异常!!");
		}
		jedis.close();
		return true;
	}

恢复库存,重新测试 结果

在这里插入图片描述
在这里插入图片描述

这样就不会出现之前那种 成功失败穿插的问题了,一个线程再用的时候不会被其他线程插队,抢夺资源,很棒

在这里插入图片描述
在这里插入图片描述

并发下的库存遗留问题解决了

连接超时问题

最后就是连接问题了 我们用节省每次连接redis服务带来的消耗,把连接好的实例反复利用。 通过参数管理连接的行为 主要用到了 :链接池参数

  • MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
  • maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
  • MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
  • lestOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;

jedis工具类业务实现~

代码语言:javascript
复制
package com.hyc.serkill.config;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolUtil {
	private static volatile JedisPool jedisPool = null;

	private JedisPoolUtil() {
	}

	public static JedisPool getJedisPoolInstance() {
		if (null == jedisPool) {
			synchronized (JedisPoolUtil.class) {
				if (null == jedisPool) {
					JedisPoolConfig poolConfig = new JedisPoolConfig();
					//最大两百实例
					poolConfig.setMaxTotal(200);
					//最多有30左右的空闲实例
					poolConfig.setMaxIdle(32);
					//连接超时毫秒数
					poolConfig.setMaxWaitMillis(100*1000);

					poolConfig.setBlockWhenExhausted(true);
					// ping  PONG
					poolConfig.setTestOnBorrow(true);

					jedisPool = new JedisPool(poolConfig, "120.79.14.203", 6379, 60000 ,"123456");
				}
			}
		}
		return jedisPool;
	}

	//资源回收
	public static void release(JedisPool jedisPool, Jedis jedis) {
		if (null != jedis) {
			jedisPool.returnResource(jedis);
		}
	}

}

总结

我们解决了秒杀并发中的三个比较关键的问题

  1. 超卖
  2. 库存剩余(本来该卖出去的却没卖完)
  3. 连接可能会超时的问题
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-10-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • redis
    • 秒杀案例
      • 简单介绍一下
      • 连接超时问题
    • 总结
    相关产品与服务
    云数据库 Redis
    腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档