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 密码
。在 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(); } }
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"; } } }
multi
:开启事务,进入组队状态。discard
:放弃组队。exec
:执行事务。watch k1 k2 ...
:监视某些 key。如果这些 key 在事务执行之前被改动,那么操作这些 key 的事务都会被取消。unwatch
:取消对所有 key 的监视。exec 和 discard 操作会自动执行 unwatch。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.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 中引入连接池依赖:
<!-- 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"; } }
对库存进行监视,多个并发用户在进行秒杀时,都将秒杀过程放在事务中,当这些并发用户中,有一个秒杀成功后,会修改库存,这时由于监控的作用,其他用户的事务都会被取消,结果是这些并发用户中只有一个会秒杀成功,因此解决了超卖问题。
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; } }
解决了超卖和库存遗留问题。