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

redis

秒杀案例

保姆级Redis秒杀解决方案设计(lua脚本解读)_第1张图片
以上为例
我们创建一个项目
Springbooy : serkill

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

	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;
	}

前端写一个简单的表单
保姆级Redis秒杀解决方案设计(lua脚本解读)_第2张图片
之后使用阿帕奇的jmeter来测试
保姆级Redis秒杀解决方案设计(lua脚本解读)_第3张图片
保姆级Redis秒杀解决方案设计(lua脚本解读)_第4张图片
并发测试之后
会发现
保姆级Redis秒杀解决方案设计(lua脚本解读)_第5张图片
在这里插入图片描述

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

简单介绍一下

LUA脚本在Redis中的优势

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

lua脚本业务类编写

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但是看是可以看懂一些的

			//获得参数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" ;

之后在下面编写方法

	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秒杀解决方案设计(lua脚本解读)_第6张图片
这样就不会出现之前那种
成功失败穿插的问题了,一个线程再用的时候不会被其他线程插队,抢夺资源,很棒
在这里插入图片描述
并发下的库存遗留问题解决了

连接超时问题

最后就是连接问题了
我们用节省每次连接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工具类业务实现~

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. 连接可能会超时的问题

你可能感兴趣的:(Nosql数据库,lua,redis,数据库)