Redis 1/2
1 安装
- 在 Redis 官方网站上下载压缩包:
- 使用 Xftp 将 Redis 压缩包放到 /opt 目录下,并使用命令:
tar -zxvf redis-6.2.2.tar.gz
解压。
- 因为 Redis 基于 C++ 实现,需要依赖两个额外包:gcc 和 gcc-c++。在 opt 目录下使用命令:
yum install gcc
、yum install gcc-c++
,安装这两个依赖。 - 在 Redis 解压目录(redis-6.2.2)下,使用命令:
make
,编译 Redis。编译完成后,使用命令:make install
,安装 Redis。
注意1:如果在安装 gcc、gcc-c++ 时之前,使用了 make 命令,则会报错,此时需要先安装 gcc 和 gcc-c++,安装成功后,使用命令:make distclean
,清空之前的运行缓存,之后再执行第 4 步即可。
注意2:Redis 的安装目录为:/usr/local/bin
,安装在该目录下的好处是:在系统的任何位置都可以执行 Redis 命令(如启动 Redis 和关闭 Redis 的命令)。
2 启动、关闭客户端和服务器
- 进入 Redis 安装目录(/opt/Redis-6.2.2),复制
redis.conf
配置文件到 /opt 下的一个新目录(myRedisConf)中。
- 打开 myRedisConf/redis.conf 文件,修改
daemonize no
为daemonize yes
,修改之后可以让 Redis 在后台启动。
- 执行:
redis-server 被修改的redis.conf路径
,开启 Redis 服务。使用:ps -ef | grep redis
,查看是否开启成功。
- 使用:
redis-cli [-h ip地址 -p 端口号]
,打开客户端。如果不指定 ip 地址和端口号,则默认使用 Redis 服务器的 ip 地址和端口号。
- 在进入到 Redis 客户端后,输入:
ping
,如果输出:pong
,则代表客户端与服务器连接成功。
- (1)退出客户端,输入:
exit
,或按下:Ctrl + C
。
(2)在客户端内,关闭服务器,输入:shutdown
。
(3)未进入客户端时,关闭服务器,输入:redis-cli shutdown
。当服务器有多个端口时,使用:redis-cli -p 要关闭的端口号 shutdown
。
3 数据库
- Redis 有 16 个数据库,类似于数组下标,这些库从 0 开始,一直到 15。默认使用 0 号库。使用命令:
select index
,切换数据库。
- 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
- String 是 Redis 最基本的数据类型,格式为键值对形式。
- String 的 value 可以包含任何数据,如图片或序列化对象。
- String 的 value 最大为 512M。
- value 的下标从 0 开始。
- 原子性:指不会被线程调度机制打断的操作。操作一旦开始执行,就一直运行到结束。
操作字符串的常用指令:
指令 | 作用 |
---|---|
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
- List:单键多值有序可重复,有一个 key 就有一个列表。
- Redis 的列表是简单的字符串列表,按照插入顺序排序。
- List 中只能存储 String。
- List 实际上是一个双向链表。
- 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
- Set 的功能与 List 类似,区别是:无序不可重复。
- Set 中只能存储 String。
- 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
- 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
- Zset 是不可重复的有序集合(Set 是不可重复的无序集合)。
- 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 k = 1000 bytes,1 kb = 1024 bytes。没有 b 的取整。不区分大小写。
- include:类似于 jsp 中的 include,可以把配置文件中相同的部分提取出来。
- ip 地址的绑定 bind:默认情况下 bind 127.0.0.1,只允许本地访问。如果想让任何地址都可以访问,只需要将 bind 注释掉,并且关闭保护模式即可。关闭保护模式:protected-model no。
- tcp-backlog:请求到达 Redis 后,到接受处理前的队列总数。
- timeout:一个空闲的客户端维持多少秒之后被关闭。0 为永不关闭。
- TCP keepalive:每隔多长时间,检测一次客户端是否与服务器仍然保持连接。官方推荐设置 60 秒。
- daemonize:是否将服务器设置为后台进程(后台启动)。
- pidfile:存放 pid 文件的位置。
- loglevel:日志级别。从低到高分别为:debug,verbose,notice,warning。级别越高在生产环境下,推荐使用 notice 或 warning。
- logfile:日志文件名称。
- syslog:是否将 Redis 日志输出到 Linux 系统日志中。
- syslog-ident:日志的标志。
- syslog-facility:输出日志的设备。
- database:Redis 库的数量。默认为 16。
- security:
(1)临时密码:进入到 Redis 客户端之后:
获取密码:config get requirepass
。
设置密码:config set requirepass "xxx"
。
设置完成之后,再进行操作要输入密码:auth 密码
。
(2)永久密码:在 Redis 配置文件中:
requirepass:设置登录密码。 - maxclient:最大客户端连接数。
-
maxmemory:设置 Redis 可以使用的最大内存。
当内存达到上限之后,Redis 会试图移除数据,进而释放内存。移除规则可以通过:maxmemory-policy 设置。如果 Redis 无法根据移除规则释放内存,或者规则设置为:不允许移除,那么 Redis 会对申请内存的指令,如 set、lpush 等返回错误信息。 -
maxmemory-policy:内存移除策略。
volatile-lru:使用 LRU 算法(最近最少使用)移除 key。只对设置了过期时间的 key 有效。
allkeys-lru:使用 LRU 算法移除 key。
volatile-random:随机移除 key。只对设置了过期时间的 key 有效。
allkeys-random:随机移除 key。
volatile-ttl:移除 ttl 时间(剩余过期时间)最小的 key。
noeviction:不移除 key。对申请内存的指令,返回错误信息。 -
maxmemory-samples:在满足 LRU 和 ttl 算法要求的数据中,挑选出几个,作为备选的移除数据。
(1)LRU 和 ttl 算法在操作过程中,可能出现多个数据,有着相同的最近最少使用次数或剩余过期时间,这些数据都满足被移除内存的条件。此时,可以设置 maxmemory-samples,在所有满足条件的数据中,挑选出 n 个,进行进一步的筛选,最终移除那个最符合条件的数据。
(2)maxmemory-samples 一般设置为 3 - 7。数字越大,备选的移除数据越多,移除的越精确,但性能消耗也越高。
8 Jedis
-
在 Redis 配置文件中,注释掉 bind 127.0.0.1。
-
不建议关闭保护模式,建议设置 Redis 登录密码。
-
关闭 Linux 系统的防火墙:
systemctl stop firewalld
。 -
查看 Linux 的 ip 地址:
ifconfig
。 -
创建 maven 项目,引入 jedis 依赖。
<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.6.0</version> </dependency> </dependencies>
-
(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:手机验证码
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 基本代码
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 命令将其发送。
目标服务器地址:
设置库存: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
出现超卖现象。
多个用户同时发出请求,在处理时,判断库存数量都大于 1,因此都秒杀成功。但正确的场景应该是只有他们中的一个秒杀成功,这个用户秒杀成功后,将库存减 1,其他并发用户不能再进行秒杀。
此外,再多并发的情况下,可能出现连接超时现象。
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";
}
}
解决了超卖问题。
但是,此时可能发生另外一个问题:库存遗留。
当提高库存,如库存设置为: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;
}
}
解决了超卖和库存遗留问题。