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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 保姆级Redis秒杀解决方案设计(lua脚本解读)

redis

秒杀案例

1.png

以上为例

我们创建一个项目

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

前端写一个简单的表单

2.png

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


3.png4.png

并发测试之后

会发现

5.png

6.png


有库存遗留,并没有卖完,这里并发的并发问题可以用脚本语言 : 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;
  }

恢复库存,重新测试

结果

1.png

这样就不会出现之前那种

成功失败穿插的问题了,一个线程再用的时候不会被其他线程插队,抢夺资源,很棒

2.png

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


连接超时问题

最后就是连接问题了

我们用节省每次连接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);
    }
  }
}

总结

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


超卖

库存剩余(本来该卖出去的却没卖完)

连接可能会超时的问题


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
18天前
|
缓存 监控 NoSQL
【Redis性能瓶颈揭秘】「调优系列」深入分析热Key的排查策略和解决方案
【Redis性能瓶颈揭秘】「调优系列」深入分析热Key的排查策略和解决方案
62 0
|
10天前
|
NoSQL Java Redis
lua脚本做redis的锁
这段内容是关于使用Redis实现分布式锁的Java代码示例。`RedisLock`类包含`lock`和`unlock`方法,使用`StringRedisTemplate`和Lua脚本进行操作。代码展示了两种加锁方式:一种带有过期时间,另一种不带。还提到了在加锁和解锁过程中的异常处理,并提供了相关参考资料链接。
17 3
|
13天前
|
存储 NoSQL 数据处理
Redis Lua脚本:赋予Redis更强大的逻辑与功能
Redis Lua脚本:赋予Redis更强大的逻辑与功能
|
26天前
|
监控
通过Lua脚本实现禁止员工上班玩游戏的软件的自动化任务管理
使用Lua脚本,企业可以自动化管理员工行为,防止上班时间玩游戏。Lua是一种轻量级脚本语言,适合编写监控任务。示例脚本展示了如何检测工作时间内员工是否玩游戏,并在发现时执行相应操作,如关闭游戏或发送警告。此外,另一脚本演示了如何将监控数据通过HTTP POST自动提交到网站,以实现有效的行为管理。这种解决方案灵活且可定制,有助于提升工作效率。
86 1
|
1月前
|
NoSQL Java 数据库
优惠券秒杀案例 - CAS、Redis+Lua脚本解决高并发并行
优惠券秒杀案例 - CAS、Redis+Lua脚本解决高并发并行
|
3月前
|
存储 NoSQL 关系型数据库
使用lua脚本操作redis
使用lua脚本操作redis
48 0
|
3月前
|
NoSQL Java Redis
Redis进阶-lua脚本
Redis进阶-lua脚本
56 0
|
1月前
|
缓存 NoSQL Java
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性
【Redis】5、Redis 的分布式锁、Lua 脚本保证 Redis 命令的原子性
55 0
|
2月前
|
算法 NoSQL Java
springboot整合redis及lua脚本实现接口限流
springboot整合redis及lua脚本实现接口限流
63 0
|
1月前
|
Java API Maven