tar -zxvf redis-6.2.2.tar.gz
解压。yum install gcc
、yum install gcc-c++
,安装这两个依赖。make
,编译 Redis。编译完成后,使用命令:make install
,安装 Redis。make distclean
,清空之前的运行缓存,之后再执行第 4 步即可。/usr/local/bin
,安装在该目录下的好处是:在系统的任何位置都可以执行 Redis 命令(如启动 Redis 和关闭 Redis 的命令)。redis.conf
配置文件到 /opt 下的一个新目录(myRedisConf)中。daemonize no
为 daemonize yes
,修改之后可以让 Redis 在后台启动。redis-server 被修改的redis.conf路径
,开启 Redis 服务。使用:ps -ef | grep redis
,查看是否开启成功。redis-cli [-h ip地址 -p 端口号]
,打开客户端。如果不指定 ip 地址和端口号,则默认使用 Redis 服务器的 ip 地址和端口号。ping
,如果输出:pong
,则代表客户端与服务器连接成功。exit
,或按下:Ctrl + C
。shutdown
。redis-cli shutdown
。当服务器有多个端口时,使用:redis-cli -p 要关闭的端口号 shutdown
。select index
,切换数据库。指令 | 作用 |
---|---|
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 |
清空全部库。 |
操作字符串的常用指令:
指令 | 作用 |
---|---|
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。 |
操作 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。 |
操作 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 中没有的数据。 |
操作 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 中。 |
操作 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 开始。 |
config get requirepass
。config set requirepass "xxx"
。auth 密码
。创建 maven 项目,引入 jedis 依赖。
<dependencies>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.6.0version>
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();
}
}
<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 heretitle>
<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";
}
}
}
multi
:开启事务,进入组队状态。discard
:放弃组队。exec
:执行事务。watch k1 k2 ...
:监视某些 key。如果这些 key 在事务执行之前被改动,那么操作这些 key 的事务都会被取消。unwatch
:取消对所有 key 的监视。exec 和 discard 操作会自动执行 unwatch。seckill.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 heretitle>
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.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,其他并发用户不能再进行秒杀。
连接池参数:
pom.xml 中引入连接池依赖:
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.6.0version>
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";
}
}
对库存进行监视,多个并发用户在进行秒杀时,都将秒杀过程放在事务中,当这些并发用户中,有一个秒杀成功后,会修改库存,这时由于监控的作用,其他用户的事务都会被取消,结果是这些并发用户中只有一个会秒杀成功,因此解决了超卖问题。
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 件商品遗留。
并且,使用事务+监视实现的秒杀,不符合生活实际。在实际秒杀中,是每个用户,不论并发与否,谁的网速快,谁先执行完代码,谁秒杀成功。不可能出现,先秒杀的用户秒杀失败,后秒杀的用户反而秒杀成功的状况。
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;
}
}