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

2021-10-19 15:55:45 浏览数 (1)

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];rn"  
			"local prodid=KEYS[2];rn"  
			"local qtkey='sk'..prodid.."qt";rn"  
			"local usersKey='sk'..prodid..":usr"rn"  
			"local userExists=redis.call("sismember",usersKey,userid);rn"  
			"if tonumber(userExists)==1 then rn"  
			"   return 2;rn"  
			"endrn"  
			"local num= redis.call("get" ,qtkey);rn"  
			"if tonumber(num)<=0 then rn"  
			"   return 0;rn"  
			"else rn"  
			"   redis.call("decr",qtkey);rn"  
			"   redis.call("sadd",usersKey,userid);rn"  
			"endrn"  
			"return 1" ;

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

脚本代码解读

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

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

0 人点赞