Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀

Redis 1/2

1 安装

  1. 在 Redis 官方网站上下载压缩包:
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
  2. 使用 Xftp 将 Redis 压缩包放到 /opt 目录下,并使用命令:tar -zxvf redis-6.2.2.tar.gz 解压。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
  3. 因为 Redis 基于 C++ 实现,需要依赖两个额外包:gcc 和 gcc-c++。在 opt 目录下使用命令:yum install gccyum install gcc-c++,安装这两个依赖。
  4. 在 Redis 解压目录(redis-6.2.2)下,使用命令:make,编译 Redis。编译完成后,使用命令:make install,安装 Redis。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
    注意1:如果在安装 gcc、gcc-c++ 时之前,使用了 make 命令,则会报错,此时需要先安装 gcc 和 gcc-c++,安装成功后,使用命令:make distclean,清空之前的运行缓存,之后再执行第 4 步即可。
    注意2:Redis 的安装目录为:/usr/local/bin,安装在该目录下的好处是:在系统的任何位置都可以执行 Redis 命令(如启动 Redis 和关闭 Redis 的命令)。

2 启动、关闭客户端和服务器

  1. 进入 Redis 安装目录(/opt/Redis-6.2.2),复制 redis.conf 配置文件到 /opt 下的一个新目录(myRedisConf)中。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
  2. 打开 myRedisConf/redis.conf 文件,修改 daemonize nodaemonize yes,修改之后可以让 Redis 在后台启动。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
  3. 执行:redis-server 被修改的redis.conf路径,开启 Redis 服务。使用:ps -ef | grep redis,查看是否开启成功。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
  4. 使用:redis-cli [-h ip地址 -p 端口号],打开客户端。如果不指定 ip 地址和端口号,则默认使用 Redis 服务器的 ip 地址和端口号。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
  5. 在进入到 Redis 客户端后,输入:ping,如果输出:pong,则代表客户端与服务器连接成功。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
  6. (1)退出客户端,输入:exit,或按下:Ctrl + C
    (2)在客户端内,关闭服务器,输入:shutdown
    (3)未进入客户端时,关闭服务器,输入:redis-cli shutdown。当服务器有多个端口时,使用:redis-cli -p 要关闭的端口号 shutdown
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀

3 数据库

  1. Redis 有 16 个数据库,类似于数组下标,这些库从 0 开始,一直到 15。默认使用 0 号库。使用命令:select index,切换数据库。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
  2. Redis 统一密码管理,所有数据库都是相同的密码。要么都能连接,要么一个也连接不上。

4 单线程+多路IO复用

5 基本指令

指令 作用
keys * 查看当前库中的所有键。
exists k 查看当前库中是否有键 k。有返回 1,没有返回 0。
type k 查看键 k 的类型。
del k 删除键 k。删除成功返回 1。
expire k time 为键 k 设置过期时间,单位为秒。
ttl k 查看键 k 还有多少秒过期。-1 表示永不过期,-2 表示已经过期。
dbsize 查看当前库中键的总数。
Flushdb 清空当前库。
Flushall 清空全部库。

6 五个基本数据类型

6.1 String

  1. String 是 Redis 最基本的数据类型,格式为键值对形式。
  2. String 的 value 可以包含任何数据,如图片或序列化对象。
  3. String 的 value 最大为 512M。
  4. value 的下标从 0 开始。
  5. 原子性:指不会被线程调度机制打断的操作。操作一旦开始执行,就一直运行到结束。

操作字符串的常用指令:

指令 作用
get k 获取 k 对应的 v。
set k v 向库中添加键值对。
append k v 在 k 的原值后追加 v。
strlen k 获取 v 的长度。
setnx k v 当 k 不存在时,设置键值对。
incr k 将 k 中存储的数字值加 1。只能对数字值操作,如果为空,则设置为 1。
decr k 将 k 中存储的数字值减 1。只能对数字值操作,如果为空,则设置为 -1。
incrby/decrby k n 将 k 中存储的数字值加或减 n。只能对数字值操作。
mset k1 v1 k2 v2 ... 同时添加多个键值对。
mget k1 k2 ... 同时获取多个 k 的 v。
msetnx k1 v1 k2 v2 ... 当 k1、k2 …都不存在时,同时设置多个键值对。
getrange k start end 从 start 开始到 end 结束,获取 k 的 v(包含 end)。相当于截取子串。
setrange k start v 从 start 开始,将 k 的原值,替换为 v。
setex k expireTime v 设置键值对的同时,设置 k 的过期时间。单位为秒。
getset k v 获取键值对,同时修改 k 的值为 v。

6.2 List

  1. List:单键多值有序可重复,有一个 key 就有一个列表。
  2. Redis 的列表是简单的字符串列表,按照插入顺序排序。
  3. List 中只能存储 String。
  4. List 实际上是一个双向链表。
  5. List 的第一个值的索引为 1,最后一个值索引为 -1。

操作 List 的常用指令:

指令 作用
lpush/rpush k v1,v2 ... 向列表 k 的头或尾插入数据。
lpop/rpop k 取出表头或表尾的值。取出之后,该值在 k 中就不存在了。
rpoplpush k1 k2 取出 k1 的表尾值插入到 k2 的表头。
lrange k start end 从左向右查看列表 k 的 [start, end] 值。
lindex k index 从左向右查看列表 k 中,索引为 index 的值。
llen k 获取列表 k 的长度。
linsert k before|after v nV 在表 k 的值 v 之前或之后插入新值 nV。
lrem k n v (1)n > 0 时:从左向右删除列表 k 中的 n 个 v;
(2)n < 0 时:从右向左删除列表 k 中的 n 个 v;
(3)n = 0 时:删除列表 k 中的全部 v。

6.3 Set

  1. Set 的功能与 List 类似,区别是:无序不可重复
  2. Set 中只能存储 String。
  3. Set 实际上是一个 hash 表。

操作 Set 的常用指令:

指令 作用
sadd k v1,v2 ... 向集合 k 中添加数据 v1,v2 …。跳过已经存在的数据。
smembers k 查看集合 k 中的所有值。
sismember k v 判断集合 k 中是否有 v。有返回 1,没有返回 0。
scard k 返回集合 k 中值的个数。
srem k v1,v2 ... 删除集合 k 中的 v1,v2 …
spop k 随机取出集合 k 中的一个值。取出之后,该值在 k 中就不存在了。
srandmember k n 随机取出集合 k 中的 n 个值。取出之后,这些值不会被删除。
sinter k1,k2 返回 k1,k2 的交集。
sunion k1,k2 返回 k1,k2 的并集。
sdiff k1,k2 返回 k1,k2 的差集。k1 - k2:k1 中有,k2 中没有的数据。

6.4 Hash

  1. Hash 是一个 String 类型的键值对集合。类似于 Java 中的 Map<String,String>

操作 Hash 的常用指令:

指令 作用
hset k f v 给 k 集合的键 f 赋值 v。
hmset k f1 v1 f2 v2 ... 批量赋值。
hget k f 获取集合 k 中,键 f 的值。
hexists k f 判断集合 k 中是否存在键 f。
hkeys k 显示集合 k 的全部键 f。
hvals k 显示集合 k 的全部值 v。
hgetall k 显示集合 k 的全部键值对。
hincrby k f increment 将集合 k 中的键 f 增加增量 increment。值要为数字类型。
hsetnx k f v 当 k 中不存在键 f 时,将 f v 保存到 k 中。

6.5 Zset

  1. Zset 是不可重复的有序集合(Set 是不可重复的无序集合)。
  2. Zset 的每个成员都关联了一个评分 score,zset 按照这个评分对成员进行排序。

操作 Zset 的常用指令:

指令 作用
zadd k s1 v1 s2 v2 ... 向集合 k 中添加成员及其所对应的评分。
(1)s,v 都相同:添加失败;
(2)s 不同,v 相同:更新 v 的 s;
(3)s 相同,v 不同:添加成功,按照添加的顺序排序。
zrange k start end 查询集合 k 中,索引在 [start, end] 中的数据。
最后一个值的索引为 -1。
从小到大排序。
zrevrange k start end 从大到小排序。
zrangebyscore k min max 查询集合 k 中,评分在 [min, max] 中的数据。
从小到大排序。
zrevrangebyscore k max min 从大到小排序。
zincrby k increment v 将值 v 的 score 增加增量 increment。
zrem k v 删除 v。
zcount k min max 返回分数在 [min, max] 之间的元素个数。
zrank k v 获取 v 在集合中的排名。排名从 0 开始。

7 配置文件

  1. 计量单位:1 k = 1000 bytes,1 kb = 1024 bytes。没有 b 的取整。不区分大小写
  2. include:类似于 jsp 中的 include,可以把配置文件中相同的部分提取出来。
  3. ip 地址的绑定 bind:默认情况下 bind 127.0.0.1,只允许本地访问。如果想让任何地址都可以访问,只需要将 bind 注释掉,并且关闭保护模式即可。关闭保护模式:protected-model no
  4. tcp-backlog:请求到达 Redis 后,到接受处理前的队列总数。
  5. timeout:一个空闲的客户端维持多少秒之后被关闭。0 为永不关闭。
  6. TCP keepalive:每隔多长时间,检测一次客户端是否与服务器仍然保持连接。官方推荐设置 60 秒。
  7. daemonize:是否将服务器设置为后台进程(后台启动)。
  8. pidfile:存放 pid 文件的位置。
  9. loglevel:日志级别。从低到高分别为:debug,verbose,notice,warning。级别越高在生产环境下,推荐使用 notice 或 warning。
  10. logfile:日志文件名称。
  11. syslog:是否将 Redis 日志输出到 Linux 系统日志中。
  12. syslog-ident:日志的标志。
  13. syslog-facility:输出日志的设备。
  14. database:Redis 库的数量。默认为 16。
  15. security:
    (1)临时密码:进入到 Redis 客户端之后:
    获取密码:config get requirepass
    设置密码:config set requirepass "xxx"
    设置完成之后,再进行操作要输入密码:auth 密码
    (2)永久密码:在 Redis 配置文件中:
    requirepass:设置登录密码。
  16. maxclient:最大客户端连接数。
  17. maxmemory:设置 Redis 可以使用的最大内存。
    当内存达到上限之后,Redis 会试图移除数据,进而释放内存。移除规则可以通过:maxmemory-policy 设置。如果 Redis 无法根据移除规则释放内存,或者规则设置为:不允许移除,那么 Redis 会对申请内存的指令,如 set、lpush 等返回错误信息。
  18. maxmemory-policy:内存移除策略。
    volatile-lru:使用 LRU 算法(最近最少使用)移除 key。只对设置了过期时间的 key 有效。
    allkeys-lru:使用 LRU 算法移除 key。
    volatile-random:随机移除 key。只对设置了过期时间的 key 有效。
    allkeys-random:随机移除 key。
    volatile-ttl:移除 ttl 时间(剩余过期时间)最小的 key。
    noeviction:不移除 key。对申请内存的指令,返回错误信息。
  19. maxmemory-samples:在满足 LRU 和 ttl 算法要求的数据中,挑选出几个,作为备选的移除数据。
    (1)LRU 和 ttl 算法在操作过程中,可能出现多个数据,有着相同的最近最少使用次数或剩余过期时间,这些数据都满足被移除内存的条件。此时,可以设置 maxmemory-samples,在所有满足条件的数据中,挑选出 n 个,进行进一步的筛选,最终移除那个最符合条件的数据。
    (2)maxmemory-samples 一般设置为 3 - 7。数字越大,备选的移除数据越多,移除的越精确,但性能消耗也越高。

8 Jedis

  1. 在 Redis 配置文件中,注释掉 bind 127.0.0.1。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀

  2. 不建议关闭保护模式,建议设置 Redis 登录密码。
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀

  3. 关闭 Linux 系统的防火墙:systemctl stop firewalld
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀

  4. 查看 Linux 的 ip 地址:ifconfig
    Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀

  5. 创建 maven 项目,引入 jedis 依赖。

    <dependencies>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.6.0</version>
        </dependency>
    </dependencies>
    
  6. (1)创建 Jedis 对象,构造器传入 Linux 系统的 ip 地址和 Redis 的端口号(默认为:6379)。
    (2)使用 auth 方法输入登录密码。
    (3)使用 ping 方法查看是否连接成功。
    (4)进行数据操作。
    (5)关闭 jedis 连接。

    public class TestJedis {
        public static void main(String[] args) {
            //构造方法,传入ip地址和端口号
            Jedis jedis = new Jedis("192.168.61.128", 6379);
            jedis.auth("Redis密码");
            String ping = jedis.ping();
            System.out.println(ping);//pong
            // jedis.set("jedisKey","jedisVal");
            String jedisKey = jedis.get("jedisKey");
            System.out.println(jedisKey);//jedisVal
            jedis.close();
        }
    }
    

9 案例1:手机验证码

Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
code.html:页面。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
	<title>Insert title here</title>
	<script  src="jquery/jquery-3.1.0.js" ></script>
	<link href="bs/css/bootstrap.min.css" rel="stylesheet" />
	<script  src="static/bs/js/bootstrap.min.js"   ></script>
</head>
<body>
<div class="container">
	<div class="row">
		<div id="alertdiv" class="col-md-12">
			<form class="navbar-form navbar-left" role="search" id="codeform">
				<div class="form-group">
					<input type="text" class="form-control" placeholder="填写手机号" name="phone_no">
					<button type="button" class="btn btn-default" id="sendCode">发送验证码</button><br>
					<font id="countdown" color="red" ></font>
					<br>
					<input type="text" class="form-control" placeholder="填写验证码" name="verify_code">
					<button type="button" class="btn btn-default" id="verifyCode">确定</button>
					<font id="result" color="green" ></font><font id="error" color="red" ></font>
				</div>
			</form>
		</div>
	</div>
</div>

</body>
<script type="text/javascript">
	var t=120;//设定倒计时的时间
	var interval;
	function refer(){
		$("#countdown").text("请于"+t+"秒内填写验证码 "); // 显示倒计时
		t--; // 计数器递减
		if(t<=0){
			clearInterval(interval);
			$("#countdown").text("验证码已失效,请重新发送! ");
		}
	}

	$(function(){
		$("#sendCode").click( function () {

			$.post("/sendcode",$("#codeform").serialize(),function(data){
				if(data=="true"){
					t=120;
					clearInterval(interval);
					interval= setInterval("refer()",1000);//启动1秒定时
				}else if (data=="limit"){
					clearInterval(interval);
					$("#countdown").text("单日发送超过次数! ")
				}
			});
		});

		$("#verifyCode").click( function () {

			$.post("/verifycode",$("#codeform").serialize(),function(data){
				if(data=="true"){
					$("#result").attr("color","green");
					$("#result").text("验证成功");
					clearInterval(interval);
					$("#countdown").text("");
				}else{
					$("#result").attr("color","red");
					$("#result").text("验证失败");
				}
			});
		});

	});
</script>
</html>

PageController.java:访问 code.html。

@Controller
public class PageController {
    @GetMapping("/code")
    public String gotoIndex(){
        return "code";
    }
}

GetCode.java:获取随机 6 位验证码。

public class GetCode {
    //生成验证码
    public static String getCode(){
        Random random = new Random();
        //随机生成6为验证码
        String code = "";
        for(int i=0; i<6; i++){
            int anInt = random.nextInt(10);
            code = code + anInt;
        }
        return code;
    }
}

CodeController.java:获取验证码以及验证验证码是否正确。

/**
 * 处理获取验证码和验证验证码业务
 */
@Controller
@ResponseBody
public class CodeController {
    private Jedis jedis = new Jedis("192.168.61.128",6379);

    @PostMapping("/sendcode")
    public String sendCode(@RequestParam("phone_no") String phoneNum){
        jedis.auth("Redis密码");
        //如果count为空,代表第一次申请验证码,申请成功,并将count设置为1
        String codeKey = "verifycode:code:"+phoneNum;
        String countKey = "verifycode:count:"+phoneNum;
        String count = jedis.get("verifycode:phone:count");
        if(count==null){
            String code = GetCode.getCode();
            //验证码有效时间为120秒
            jedis.setex(codeKey,120,code);
            //24h之内之内获取3次验证码
            jedis.setex(countKey,24*60*60,"1");
        }else if(Integer.parseInt(count) <= 2){
            //如果count<=2,作则还可以申请,发送验证码,将count+1
            String code = GetCode.getCode();
            jedis.setex(codeKey,120,code);
            jedis.incr(countKey);
        }else if(Integer.parseInt(count) >= 3){
            //如果count>=3,则不可以再申请
            jedis.close();
            return "limit";
        }
        jedis.close();
        return "true";
    }

    /**
     * 验证验证码是否正确
     */
    @PostMapping("/verifycode")
    public String verifyCode(@RequestParam("phone_no") String phoneNum,
                             @RequestParam("verify_code") String verifyCode){
        String codeKey = "verifycode:code:"+phoneNum;
        String code = jedis.get(codeKey);
        if(code==null){
            jedis.close();
            return "nocode";
        }else if(code.equals(verifyCode)){
            jedis.close();
            return "true";
        }else{
            jedis.close();
            return "false";
        }
    }
}

10 事务

  • Redis 事务是一个单独的隔离操作,事务中的所有命令都会序列化,按顺序执行。事务在执行的过程中,不会被其他客户端发来的请求打断。
  • Redis 事务的主要作用是串联多个命令,放置正在执行的命令被其他命令打断。
  • 事务处理命令
    (1)multi:开启事务,进入组队状态。
    (2)discard:放弃组队。
    (3)exec:执行事务。
    (4)watch k1 k2 ...:监视某些 key。如果这些 key 在事务执行之前被改动,那么操作这些 key 的事务都会被取消。
    (5)unwatch:取消对所有 key 的监视。exec 和 discard 操作会自动执行 unwatch
  • 事务的错误处理
    (1)组队时某个命令出现书写错误:整个组队的队列都会被取消。
    (2)执行阶段某个命令出现错误:出错的命令被取消,其他命令继续执行。
  • 三个特性
    (1)单独的隔离操作。
    (2)没有隔离级别的概念。
    (3)不保证原子性。

11 案例2:秒杀

11.1 基本代码

Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀

seckill.html:页面。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>iPhoneXsMAX !!!  1元秒杀!!!
</h1>


<form id="msform" action="" th:action="@{/doSecKill}" enctype="application/x-www-form-urlencoded">
	<input type="hidden" id="prodid" name="prodid" value="0101">
	<input type="button"  id="miaosha_btn" name="seckill_btn" value="秒杀点我"/>
</form>

</body>
<script  type="text/javascript" src="jquery/jquery-3.1.0.js"></script>
<script  type="text/javascript">
$(function(){
	$("#miaosha_btn").click(function(){	 
		var url=$("#msform").attr("action");
	     $.post(url,$("#msform").serialize(),function(data){
     		if(data=="nostock"){
    			alert("抢光了" );
    			$("#miaosha_btn").attr("disabled",true);
    		}else if(data=="havesuccess"){
				alert("您已经秒杀成功,不能再次秒杀" );
				$("#miaosha_btn").attr("disabled",true);
			}else if(data=="success"){
				alert("秒杀成功" );
				$("#miaosha_btn").attr("disabled",true);
			}
		} );    
	})
})
</script>
</html>

PageController.java:访问 seckill.html。

@Controller
public class PageController {
    @GetMapping("/seckill")
    public String gotoSeckill(){
        return "seckill";
    }
}

SecKillController.java

@Controller
public class SecKillController {
    @ResponseBody
    @PostMapping("/doSecKill")
    public String secKill(@RequestParam("prodid") String prodid) throws IOException {
        //随机生成userid
        String userid = new Random().nextInt(50000) +"" ;
        String status= SecKill_redis.doSecKill(userid,prodid);
        return status;
    }
}

SecKill_redis.java:处理秒杀逻辑。

public class SecKill_redis {
	
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redis.class) ;
	
	public static String doSecKill(String uid,String prodid) throws IOException {
		//连接Redis
		Jedis jedis = new Jedis("192.168.61.128", 6379);
		jedis.auth("Redis密码");
		//向Redis中存key
		String stockKey = "seckill:"+prodid+":stock";
		String userKey = "seckill:"+prodid+":user";
		//获取库存
		String stock = jedis.get(stockKey);
		//商品是否有库存,若没有,则显示秒杀结束
		if(stock==null||Integer.parseInt(stock)<=0){
			jedis.close();
			System.out.println("秒杀已经结束");
			return "nostock";
		}else if(jedis.sismember(userKey,uid)){//用户是否已经秒杀成功,秒杀成功的用户不能继续秒杀
			jedis.close();
			System.out.println("已秒杀,不能再秒杀");
			return "havesuccess";
		}
		//其他情况正常判断,库存减1,并添加秒杀成功的用户的id
		jedis.decr(stockKey);
		jedis.sadd(userKey,uid);
		jedis.close();
		System.out.println("秒杀成功");
		return "success";
	}
}

11.2 使用 ab 工具模拟并发

在 11.1 中,基本代码并没有考虑并发场合。使用 ab 工具模拟并发。
CentOS 6 默认安装 ab 工具;CentOS 7 需要手动安装。

在 Linux 系统下,使用命令:yum install httpd-tools,安装 ab 工具。
使用命令:ab -n 请求数 -c 并发数 -p 存储要发送的参数的文件 -T 发送参数的格式 请求地址,模拟并发。

在本例中,表单要发送 prodid=0101。因此,在 Linux 本地新建文件,存放这个参数,之后使用 ab 命令将其发送。
Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
目标服务器地址:
Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
设置库存:set seckill:0101:stock 20

使用命令:ab -n 2000 -c 200 -p /opt/postfile -T application/x-www-form-urlencoded http://192.168.0.154:8080/doSecKill,发送并发请求。

查看剩余库存:get seckill:0101:stock
Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
出现超卖现象
Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
多个用户同时发出请求,在处理时,判断库存数量都大于 1,因此都秒杀成功。但正确的场景应该是只有他们中的一个秒杀成功,这个用户秒杀成功后,将库存减 1,其他并发用户不能再进行秒杀。

此外,再多并发的情况下,可能出现连接超时现象。
Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀

11.3 使用 Redis 数据库连接池解决连接超时问题

连接池参数:

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

pom.xml 中引入连接池依赖:

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
</dependency>

JedisPoolUtil.java:获取数据库连接池。

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  PONG

					jedisPool = new JedisPool(poolConfig, "192.168.61.128", 6379,
							60000,  "Redis密码");
				}
			}
		}
		return jedisPool;
	}

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

SecKill_redis.java

public class SecKill_redis {
	
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redis.class) ;
	
	public static String doSecKill(String uid,String prodid) throws IOException {
		//使用Redis数据库连接池,解决连接超时问题。
		//获取数据库连接池
		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		//获取Jedis连接
		Jedis jedis = jedisPoolInstance.getResource();
		
		//向Redis中存key
		String stockKey = "seckill:"+prodid+":stock";
		String userKey = "seckill:"+prodid+":user";
		//获取库存
		String stock = jedis.get(stockKey);
		//商品是否有库存,若没有,则显示秒杀结束
		if(stock==null||Integer.parseInt(stock)<=0){
			jedis.close();
			System.out.println("秒杀已经结束");
			return "nostock";
		}else if(jedis.sismember(userKey,uid)){//用户是否已经秒杀成功,秒杀成功的用户不能继续秒杀
			jedis.close();
			System.out.println("已秒杀,不能再秒杀");
			return "havesuccess";
		}
		//其他情况正常判断,库存减1,并添加秒杀成功的用户的id
		jedis.decr(stockKey);
		jedis.sadd(userKey,uid);
		jedis.close();
		System.out.println("秒杀成功");
		return "success";
	}
}

11.4 使用事务+监控解决超卖问题

对库存进行监视,多个并发用户在进行秒杀时,都将秒杀过程放在事务中,当这些并发用户中,有一个秒杀成功后,会修改库存,这时由于监控的作用,其他用户的事务都会被取消,结果是这些并发用户中只有一个会秒杀成功,因此解决了超卖问题。

SecKillController.java

@Controller
public class SecKillController {
    @ResponseBody
    @PostMapping("/doSecKill")
    public String secKill(@RequestParam("prodid") String prodid) throws IOException {
        //随机生成userid
        String userid = new Random().nextInt(50000) +"" ;
        String status= SecKill_redis.doSecKill(userid,prodid);
        return status;
    }
}

SecKill_redis.java

public class SecKill_redis {
	
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redis.class) ;
	
	public static String doSecKill(String uid,String prodid) throws IOException {
		//使用Redis数据库连接池,解决连接超时问题。
		//获取数据库连接池
		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		//获取Jedis连接
		Jedis jedis = jedisPoolInstance.getResource();
		
		//向Redis中存key
		String stockKey = "seckill:"+prodid+":stock";
		String userKey = "seckill:"+prodid+":user";
		
		//监视库存
		jedis.watch(stockKey);
		
		//获取库存
		String stock = jedis.get(stockKey);
		//商品是否有库存,若没有,则显示秒杀结束
		if(stock==null||Integer.parseInt(stock)<=0){
			jedis.close();
			System.out.println("秒杀失败");
			return "nostock";
		}else if(jedis.sismember(userKey,uid)){//用户是否已经秒杀成功,秒杀成功的用户不能继续秒杀
			jedis.close();
			System.out.println("已秒杀,不能再秒杀");
			return "havesuccess";
		}
		
		//开启事务
		Transaction transaction = jedis.multi();
		//开启事务后,要在事务中进行的操作,由事务对象完成
		//其他情况正常判断,库存减1,并添加秒杀成功的用户的id
		transaction.decr(stockKey);
		transaction.sadd(userKey,uid);
		//执行事务
		List<Object> exec = transaction.exec();
		//判断事务是否执行成功。执行成功List中有每个命令的执行结果,执行失败List为空或size=0
		if(exec==null || exec.size()==0){
			System.out.println("秒杀失败");
			jedis.close();
			return "nostock";
		}
		
		jedis.close();
		System.out.println("秒杀成功");
		return "success";
	}
}

Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
解决了超卖问题。

但是,此时可能发生另外一个问题:库存遗留
Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
当提高库存,如库存设置为:500 时,秒杀结束后,剩余库存不是 0,而是 230。

并发进程之间只有一个能秒杀成功,其他用户都秒杀失败,当秒杀失败的进程不再继续秒杀时,就会发生库存遗留。这在生活中很常见,比如一共 5 个库存,800 个请求,每 200 个请求是一个并发进程,当 200 个并发用户进程进行秒杀时,只有一个秒杀成功,这时其他 199 个用户不再继续秒杀,这样进行下去,只有 4 个用户秒杀成功,造成 1 件商品遗留。

并且,使用事务+监视实现的秒杀,不符合生活实际。在实际秒杀中,是每个用户,不论并发与否,谁的网速快,谁先执行完代码,谁秒杀成功。不可能出现,先秒杀的用户秒杀失败,后秒杀的用户反而秒杀成功的状况。

11.5 使用 LUA 脚本解决库存遗留问题

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

  • 将复杂的或者多步的 redis 操作,写为一个 Lua 脚本,一次提交给 redis 执行,减少反复连接 redis 的次数。提升性能。

  • Lua 脚本类似 redis 的事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作。但是注意 redis 的 Lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用

  • 可以利用 Lua 脚本解决超卖和库存遗留问题。实际上是 redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

SecKill_redisByScript.java:处理秒杀逻辑。

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

	static String secKillScript ="local userid=KEYS[1];\r\n" + 
			"local prodid=KEYS[2];\r\n" + 
			"local stockKey='seckill:'..prodid..\":stock\";\r\n" +
			"local userKey='seckill:'..prodid..\":user\";\r\n" +
			"local userExists=redis.call(\"sismember\",userKey,userid);\r\n" +
			"if tonumber(userExists)==1 then \r\n" + 
			"   return 2;\r\n" + 
			"end\r\n" + 
			"local num= redis.call(\"get\" ,stockKey);\r\n" +
			"if tonumber(num)<=0 then \r\n" + 
			"   return 0;\r\n" + 
			"else \r\n" + 
			"   redis.call(\"decr\",stockKey);\r\n" +
			"   redis.call(\"sadd\",userKey,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 String doSecKill(String uid,String prodid) throws IOException {
		//使用Redis数据库连接池,解决连接超时问题。
		//获取数据库连接池
		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		//获取Jedis连接
		Jedis jedis = jedisPoolInstance.getResource();

		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("已抢空!!");
			jedis.close();
			return "nostock";
		}else if("1".equals( reString )  )  {
			System.out.println("抢购成功!!!!");
			jedis.close();
			return "success";
		}else if("2".equals( reString )  )  {
			System.err.println("该用户已抢过!!");
			jedis.close();
			return "havesuccess";
		}else{
			System.err.println("抢购异常!!");
			jedis.close();
			return "false";
		}		 
	}
}

SecKillController.java

@Controller
public class SecKillController {
    @ResponseBody
    @PostMapping("/doSecKill")
    public String secKill(@RequestParam("prodid") String prodid) throws IOException {
        String userid = new Random().nextInt(50000) +"" ;
        String status= SecKill_redisByScript.doSecKill(userid,prodid);
        return status;
    }
}

Redis学习笔记——安装配置、5个基本数据类型、Jedis、手机验证码、秒杀
解决了超卖和库存遗留问题。

上一篇:Redis进阶-JedisCluster初始化 & 自动管理连接池中的连接 _ 源码分析


下一篇:字符串处理StringUtils方法-字符串截取