11. Redis_事务_秒杀案例

11.1. 解决计数器和人员记录的事务操作

基本功能实现:

public class SecKillController {//秒杀过程public static boolean doSecKill(String uid,String prodid){//1. uid和proid非空判断if (uid==null || prodid==null){return false;}//2. 连接redisJedis jedis = new Jedis("192.168.195.129", 6379);//3. 拼接key//3.1 库存keyString kcKey = "sk:"+prodid+":qt";//3.2 秒杀成功用户keyString userKey = "sk:"+prodid+":user";//4. 获取库存,如果库存为空,秒杀还没有开始String kc = jedis.get(kcKey);if (kc==null){System.out.println("秒杀还没开始,等待");jedis.close();return false;}//5. 判断用户是否重复秒杀操作boolean sismember = jedis.sismember(userKey, uid);if (sismember){System.out.println("秒杀成功,只能秒杀一次");jedis.close();return false;}//6. 库存的数量小于1,秒杀结束if (Integer.parseInt(kc)<=0){System.out.println("秒杀结束");jedis.close();return false;}//7. 秒杀过程//7.1 库存-1jedis.decr(kcKey);//7.2 把秒杀成功用户添加到清单里面jedis.sadd(userKey,uid);System.out.println("秒杀成功");jedis.close();return true;}
}

11.2. Redis事务–秒杀并发模拟

使用工具ab模拟测试
CentOS6 默认安装
CentOS7需要手动安装

11.2.1. 联网

yum install httpd-tools

11.2.2. 无网络

(1) 进入cd /run/media/root/CentOS 7 x86_64/Packages(路径跟centos6不同)
(2) 顺序安装
apr-1.4.8-3.el7.x86_64.rpm
apr-util-1.5.2-6.el7.x86_64.rpm
httpd-tools-2.4.6-67.el7.centos.x86_64.rpm

11.2.3. 测试及结果

11.2.3.1. 通过ab测试

[root@localhost ~]# ab --help
ab: wrong number of arguments
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:-n requests     Number of requests to perform-c concurrency  Number of multiple requests to make at a time-t timelimit    Seconds to max. to spend on benchmarkingThis implies -n 50000-s timeout      Seconds to max. wait for each responseDefault is 30 seconds-b windowsize   Size of TCP send/receive buffer, in bytes-B address      Address to bind to when making outgoing connections-p postfile     File containing data to POST. Remember also to set -T-u putfile      File containing data to PUT. Remember also to set -T-T content-type Content-type header to use for POST/PUT data, eg.'application/x-www-form-urlencoded'Default is 'text/plain'-v verbosity    How much troubleshooting info to print-w              Print out results in HTML tables-i              Use HEAD instead of GET-x attributes   String to insert as table attributes-y attributes   String to insert as tr attributes-z attributes   String to insert as td or th attributes-C attribute    Add cookie, eg. 'Apache=1234'. (repeatable)-H attribute    Add Arbitrary header line, eg. 'Accept-Encoding: gzip'Inserted after all normal header lines. (repeatable)-A attribute    Add Basic WWW Authentication, the attributesare a colon separated username and password.-P attribute    Add Basic Proxy Authentication, the attributesare a colon separated username and password.-X proxy:port   Proxyserver and port number to use-V              Print version number and exit-k              Use HTTP KeepAlive feature-d              Do not show percentiles served table.-S              Do not show confidence estimators and warnings.-q              Do not show progress when doing more than 150 requests-g filename     Output collected data to gnuplot format file.-e filename     Output CSV file with percentages served-r              Don't exit on socket receive errors.-h              Display usage information (this message)-Z ciphersuite  Specify SSL/TLS cipher suite (See openssl ciphers)-f protocol     Specify SSL/TLS protocol(SSL3, TLS1, TLS1.1, TLS1.2 or ALL)

vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。
内容:prodid=0101&

ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.2.115:8081/Seckill/doseckill

通过浏览器测试

ab -n 1000 -c 100 http://192.168.137.1:8080/seckill

11.2.3.2. 超卖


11.3. 超卖问题

11.4. 利用乐观锁淘汰用户,解决超卖问题。


增加乐观锁:

//增加乐观锁
jedis.watch(qtkey);//3.判断库存
String qtkeystr = jedis.get(qtkey);
if(qtkeystr==null || "".equals(qtkeystr.trim())) {System.out.println("未初始化库存");
jedis.close();
return false ;
}int qt = Integer.parseInt(qtkeystr);
if(qt<=0) {System.err.println("已经秒光");
jedis.close();
return false;
}


增加事务:

//增加事务
Transaction multi = jedis.multi();//4.减少库存
//jedis.decr(qtkey);
multi.decr(qtkey);//5.加人
//jedis.sadd(usrkey, uid);
multi.sadd(usrkey, uid);//执行事务
List<Object> list = multi.exec();//判断事务提交是否失败
if(list==null || list.size()==0) {System.out.println("秒杀失败");
jedis.close();
return false;
}
System.err.println("秒杀成功");
jedis.close();


完整代码:

@Controller
public class SecKillController {//秒杀过程public static boolean doSecKill(String uid,String prodid){//1. uid和proid非空判断if (uid==null || prodid==null){return false;}//2. 连接redis
//        Jedis jedis = new Jedis("192.168.195.129", 6379);//通过连接池得到jedis对象JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();Jedis jedis = jedisPoolInstance.getResource();//3. 拼接key//3.1 库存keyString kcKey = "sk:"+prodid+":qt";//3.2 秒杀成功用户keyString userKey = "sk:"+prodid+":user";//监视库存jedis.watch(kcKey);//4. 获取库存,如果库存为空,秒杀还没有开始String kc = jedis.get(kcKey);if (kc==null){System.out.println("秒杀还没开始,等待");jedis.close();return false;}//5. 判断用户是否重复秒杀操作boolean sismember = jedis.sismember(userKey, uid);if (sismember){System.out.println("秒杀成功,只能秒杀一次");jedis.close();return false;}//6. 库存的数量小于1,秒杀结束if (Integer.parseInt(kc)<=0){System.out.println("秒杀结束");jedis.close();return false;}//7. 秒杀过程//使用事务Transaction multi = jedis.multi();//组队操作multi.decr(kcKey);multi.sadd(userKey,uid);//执行List<Object> results = multi.exec();if (results==null || results.size()==0){System.out.println("秒杀失败");jedis.close();return false;}//7.1 库存-1jedis.decr(kcKey);//7.2 把秒杀成功用户添加到清单里面jedis.sadd(userKey,uid);System.out.println("秒杀成功");jedis.close();return true;}
}

JedisPoolUtil类:

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);poolConfig.setMaxIdle(32);poolConfig.setMaxWaitMillis(100*1000);poolConfig.setBlockWhenExhausted(true);poolConfig.setTestOnBorrow(true);  // ping  PONGjedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );}}}return jedisPool;}public static void release(JedisPool jedisPool, Jedis jedis) {if (null != jedis) {//          jedisPool.returnResource(jedis);jedis.close();}}}

11.5. 继续增加并发测试

11.5.1. 连接有限制

ab -n 2000 -c 200 -k -p postfile -T 'application/x-www-form-urlencoded' http://192.168.140.1:8080/seckill/doseckill

增加-r参数,-r Don’t exit on socket receive errors.

ab -n 2000 -c 100 -r -p postfile -T 'application/x-www-form-urlencoded' http://192.168.140.1:8080/seckill/doseckill

11.5.2. 已经秒光,可是还有库存

ab -n 2000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.137.1:8080/seckill/doseckill

已经秒光,可是还有库存。原因,就是乐观锁导致很多请求都失败。先点的没秒到,后点的可能秒到了。

11.5.3. 连接超时,通过连接池解决

11.5.4. 连接池

节省每次连接redis服务带来的消耗,把连接好的实例反复利用。
通过参数管理连接的行为

链接池参数

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

11.6. 解决库存遗留问题

11.6.1. LUA脚本

Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言
很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。
https://www.w3cschool.cn/lua/

11.6.2. LUA脚本在Redis中的优势

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

11.7. Redis_事务_秒杀案例(总结)

11.7.1. 项目结构

JedisPoolUtil

package com.gao.controller;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);poolConfig.setMaxIdle(32);poolConfig.setMaxWaitMillis(100*1000);poolConfig.setBlockWhenExhausted(true);poolConfig.setTestOnBorrow(true);  // ping  PONGjedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );}}}return jedisPool;}public static void release(JedisPool jedisPool, Jedis jedis) {if (null != jedis) {//          jedisPool.returnResource(jedis);jedis.close();}}}

报错解决办法:https://ask.csdn.net/questions/7683274

SecKill_redisByScript:

package com.gao.controller;import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.LoggerFactory;import ch.qos.logback.core.joran.conditional.ElseAction;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.Transaction;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";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;}
}

SecKillServlet:

package com.gao.controller;import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.ws.soap.AddressingFeature.Responses;/*** 秒杀案例*/
public class SecKillServlet extends HttpServlet {private static final long serialVersionUID = 1L;public SecKillServlet() {super();}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String userid = new Random().nextInt(50000) +"" ;String prodid =request.getParameter("prodid");//boolean isSuccess=SecKill_redis.doSecKill(userid,prodid);boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);response.getWriter().print(isSuccess);}}

SecKill_redis:

package com.gao.controller;import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.LoggerFactory;import ch.qos.logback.core.rolling.helper.IntegerTokenConverter;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.Transaction;/****/
public class SecKill_redis {public static void main(String[] args) {Jedis jedis =new Jedis("192.168.44.168",6379);System.out.println(jedis.ping());jedis.close();}//秒杀过程public static boolean doSecKill(String uid,String prodid) throws IOException {//1 uid和prodid非空判断if(uid == null || prodid == null) {return false;}//2 连接redis//Jedis jedis = new Jedis("192.168.44.168",6379);//通过连接池得到jedis对象JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();Jedis jedis = jedisPoolInstance.getResource();//3 拼接key// 3.1 库存keyString kcKey = "sk:"+prodid+":qt";// 3.2 秒杀成功用户keyString userKey = "sk:"+prodid+":user";//监视库存jedis.watch(kcKey);//4 获取库存,如果库存null,秒杀还没有开始String kc = jedis.get(kcKey);if(kc == null) {System.out.println("秒杀还没有开始,请等待");jedis.close();return false;}// 5 判断用户是否重复秒杀操作if(jedis.sismember(userKey, uid)) {System.out.println("已经秒杀成功了,不能重复秒杀");jedis.close();return false;}//6 判断如果商品数量,库存数量小于1,秒杀结束if(Integer.parseInt(kc)<=0) {System.out.println("秒杀已经结束了");jedis.close();return false;}//7 秒杀过程//使用事务Transaction multi = jedis.multi();//组队操作multi.decr(kcKey);multi.sadd(userKey,uid);//执行List<Object> results = multi.exec();if(results == null || results.size()==0) {System.out.println("秒杀失败了....");jedis.close();return false;}//7.1 库存-1//jedis.decr(kcKey);//7.2 把秒杀成功用户添加清单里面//jedis.sadd(userKey,uid);System.out.println("秒杀成功了..");jedis.close();return true;}
}

11.7.2. 第一版:简单版

老师点10次,正常秒杀
同学一起点试一试,秒杀也是正常的。这是因为还达不到并发的效果。
使用工具ab模拟并发测试,会出现超卖情况。查看库存会出现负数

11.7.3. 第二版:加事务-乐观锁(解决超卖),但出现遗留库存和连接超时

11.7.4. 第三版:连接池解决超时问题

11.7.5. 第四版:解决库存依赖问题,LUA脚本

local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr';
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;

11、Redis_事务_秒杀案例相关推荐

  1. Redis 学习笔记-NoSQL数据库 常用五大数据类型 Redis配置文件介绍 Redis的发布和订阅 Redis_事务_锁机制_秒杀 Redis应用问题解决 分布式锁

    1.NoSQL数据库 1.1 NoSQL数据库概述 NoSQL(NosQL = Not Only sQL ),意即"不仅仅是sQL",泛指非关系型的数据库.NoSQL不依赖业务逻辑 ...

  2. 秒杀 mysql 事务_秒杀怎么样才可以防止超卖?基于mysql的事务和锁实现

    Reference:  http://blog.ruaby.com/?p=256 并发事务处理带来的问题? 相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从 ...

  3. mysql悲观锁关键字_MySQL悲观锁 select for update实现秒杀案例(jfinal框架)

    MySQL悲观锁 select for update实现秒杀案例(jfinal框架) 发布时间:2018-08-17作者:laosun阅读(4287) 为了方便测试,博主使用最新的jfinal框架,里 ...

  4. Redis 秒杀案例

    Redis 秒杀案例 文章目录 Redis 秒杀案例 实现 ab工具模拟并发 超卖和超时问题解决 配置JedisPool连接池来解决超时问题 利用乐观锁淘汰用户,解决超卖问题 库存遗留问题解决 什么是 ...

  5. 2022年最新美妆护肤_策划案例合集(共13份)

    合集名称:美妆护肤_策划案例合集 报告数量:13份 具体内容: [美妆护肤案例]2020羽森VSEEN祛痘修护精华液小红书种草方案.pdf [美妆护肤案例]2021TAKAMI忠粉计划战略落地规划.p ...

  6. ArcGIS for Desktop入门教程_第四章_入门案例分析 - ArcGIS知乎-新一代ArcGIS问答社区...

    原文:ArcGIS for Desktop入门教程_第四章_入门案例分析 - ArcGIS知乎-新一代ArcGIS问答社区 1 入门案例分析 在第一章里,我们已经对ArcGIS系列软件的体系结构有了一 ...

  7. Redis学习之秒杀案例

    记录每次学习的过程,总结学习的内容,希望能给到自己和别人帮助. Redis学习之秒杀案例 基础设计思路 1.uid和prodid的非空判断 2.连接redis 3.拼接key,包括库存的和成功秒杀的用 ...

  8. Python爬虫_音乐案例

    Python爬虫_音乐案例 [案例目的]:下载音乐 [第三方库]:1.requests 2.perttytable [开发环境]:1.Python3.8 2.PyCharm 2022.1 # http ...

  9. .NET平台开源项目速览(11)KwCombinatorics排列组合使用案例(1)

    阅读目录 1.KwCombinatorics基本介绍 2.足球彩票排列组合应用 3.排列组合生成代码与测试 今年上半年,我在KwCombinatorics系列文章中,重点介绍了KwCombinator ...

最新文章

  1. (C++)CSP202006-2 稀疏向量 two pointers
  2. 存在于实数域的微观粒子3-∂f(x)/ ∂x=f(x).f(-x)
  3. angular五大服务顺序_建议收藏 | 一篇文章告诉你工种的进场顺序
  4. nginx+ssl+pm2 部署 nodejs 服务
  5. 文献记录(part92)--Graph regularized low-rank representation for submodule clustering
  6. python3怎么使用mnist_loader_Python读取mnist
  7. 此上下文中不允许函数定义。_面试官:那我们来说说执行上下文吧
  8. 2019最新好听DJ舞曲精选(51-70)【WAV/百度】
  9. 单片机项目:音乐喷泉
  10. abaqus更改计算机名,ABAQUS 2016 安装教程
  11. 软件产品需求分析思维导图
  12. Linux 下的dd命令使用详解以及dd if=/dev/zero of=的含义
  13. flink-note笔记:flink-window模块中SlidingWindow解析
  14. CeGui+Ogre
  15. 数据的离散程度的衡量方式
  16. 牛客多校补题专场 Monotonic Matrix LGV定理
  17. 微信小程序 账号密码 和 手机号 验证码 登录
  18. Python爬虫学习5:使用cookie访问网页(以豆瓣为例)
  19. 父与子的编程python_父与子的编程之旅:与小卡特一起学Python
  20. [3d跑酷] Xcode5 打包 发布配置

热门文章

  1. CentOS7查看IP并开启Ens33网卡IP
  2. SQL递归查询实现组织机构树
  3. 剑指offer 动画图解 | 跳台阶 3分钟刷题
  4. 微信小程序:用户授权登录
  5. c语言创建可视化窗口,一种基于可视化界面绘制图形自动生成C语言代码软件的设计与实现...
  6. Set集合和Map集合
  7. 小学一年级的体能测试
  8. Latex正文中插入罗马数字
  9. 文本编辑工具之vim使用介绍
  10. 猎豹MFC--画文本和字体DrawText TextOut CFont 阴影