本博客为尚硅谷课程笔记,课程来源:【尚硅谷】Redis 6 入门到精通 超详细 教程_哔哩哔哩_bilibili
部分参考博文:Redis6从入门到精通
参考:菜鸟教程-Redis配置; bilibili-尚硅谷Redis6
要将
bind 127.0.0.1
注释掉,而且还要将protected-mode
改为no
,不然后文无法通过Jedis工具连接到Redis数据库。
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。Redis客户端可以订阅任意数量的频道。
两个终端运行redis-cli
演示sub&pub
psubscribe pattern [pattern .....]
订阅一个或多个符合给定模式的频道,每个模式以 *
作为匹配符,比如it*
匹配所有以 it 开头的频道( it.news 、 it.blog 、 it.tweets 等等)。 news.*
匹配所有以 news. 开头的频道( news.it 、 news.global.today 等等),诸如此类PUBSUB subcommand [argument [argument ...]]
查看订阅与发布系统状态PUBLISH channel message
将信息发送到指定的频道PUNSUBSCRIBE [pattern [pattern ...]]
退订所有给定模式的频道SUBSCRIBE channel [channel ...]
订阅给定的一个或多个频道的信息UNSUBSCRIBE [channel [channel ...]]
指退订给定的频道首先需要检查配置文件是否按照上文的要求修改。
新建JAVA的Maven项目,依赖项如下:
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13.2version>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>4.2.3version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-simpleartifactId>
<version>1.7.25version>
<scope>compilescope>
dependency>
dependencies>
使用下面的程序测试连通性:
import org.junit.Test;
import redis.clients.jedis.Jedis;
public class JedisTest {
@Test
public void ConnectTest(){
// 创建Jedis对象 构造方法选择有参(host,port)
Jedis jedis = new Jedis("192.168.211.130", 6379);
// 连通性测试
jedis.auth("******"); // 设置的密码(可选)
String value = jedis.ping();
System.out.println(value);
}
}
输出"PONG"即代表成功。
出现SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“.的解决方法
sudo ufw status # ubuntu查看防火墙状态
# 开启和关闭防火墙
sudo ufwenable
sudo ufwdisable
######
systemctl status firewalld # centos
systemctl enable firewalld.service
systemctl disable firewalld.service
具体函数与上文的命令行操作类似,参考博文:Jedis常用API
手机验证码功能
需求如下:
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入3次
import redis.clients.jedis.Jedis;
import java.util.Random;
/**
* @author moonjay
*/
public class PhoneCode {
public static void main(String[] args) {
//模拟验证码发送
// verifyCode("13678765435");
//模拟验证码校验
getRedisCode("13678765435","120603");
}
/**1. 获取验证码*/
public static String getCode(){
Random random = new Random();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < 6; i++) {
int rand = random.nextInt(10);
sbf.append(rand);
}
return sbf.toString();
}
/**2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120s*/
public static void verifyCode(String phone) {
//连接redis
Jedis jedis = new Jedis("192.168.211.130", 6379);
jedis.auth("******");
//拼接key
//手机发送次数key
String countKey = "VerifyCode"+phone+":count";
//验证码key
String codeKey = "VerifyCode"+phone+":code";
//每个手机每天只能发送三次
String count = jedis.get(countKey);
if(count == null) {
//没有发送次数,第一次发送
//设置发送次数是1
jedis.setex(countKey,24*60*60,"1");
} else if(Integer.parseInt(count)<=2) {
//发送次数+1
jedis.incr(countKey);
} else if(Integer.parseInt(count)>2) {
//发送三次,不能再发送
System.out.println("今天发送次数已经超过三次");
jedis.close();
return ;
}
//发送验证码放到redis里面
String vcode = getCode();
jedis.setex(codeKey,120,vcode);
jedis.close();
}
/**3 验证码校验*/
public static void getRedisCode(String phone,String code) {
//从redis获取验证码
Jedis jedis = new Jedis("192.168.211.130", 6379);
jedis.auth("******");
//验证码key
String codeKey = "VerifyCode"+phone+":code";
String redisCode = jedis.get(codeKey);
//判断
if(redisCode.equals(code)) {
System.out.println("成功");
}else {
System.out.println("失败");
}
jedis.close();
}
}
SpringBoot整合
参考博文:Redis(二)jedis API
Redis事务是一个单独的隔离操作∶事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多个命令防止别的命令插队。
Redis 事务可以一次执行多个命令, 并且有以下三个重要的特性:
一个事务从开始到执行会经历以下三个阶段:
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis 会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。
multi
标记一个事务块的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
官网对此的说明【redis docs on transactions】
discard
取消事务,放弃执行事务块内的所有命令
exec
执行所有事务块内的命令,当操作被打断时,返回空值 nil
WATCH key [key ...]
监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断【乐观锁的过程】
# 监视 key ,且事务被打断
redis 127.0.0.1:6379> WATCH lock lock_times
OK
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET lock "joe" # 就在这时,另一个客户端修改了 lock_times 的值
QUEUED
redis 127.0.0.1:6379> INCR lock_times
QUEUED
redis 127.0.0.1:6379> EXEC # 因为 lock_times 被修改, joe 的事务执行失败
(nil)
UNWACTH
取消 WATCH 命令对所有 key 的监视
为什么将Redis操作做成事务?
想象一个场景,很多人有你的账户,同时去参加双十一抢购。假设有三个请求:
悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。缺点是效率低。
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种 check-and-set机制实现事务的。
参考博文:【脏读、不可重复读、幻读和MVCC】
如果不考虑事务隔离性,产生哪些读问题?
脏读(读取未提交数据)
A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。
就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。
这种情况常发生于转账与取款操作中
不可重复读(前后多次读取,数据内容不一致)
事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。
幻读(Phantom Read),是指当事务不是独立执行时发生的一种现象。
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。
不可重复读和幻读到底有什么区别呢?
(1) 不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2) 幻读是读取了其他事务新增的数据,针对insert和delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
MVCC(多版本并发控制)
英文全称为Multi-Version Concurrency Control,乐观锁为理论基础的MVCC(多版本并发控制),MVCC的实现没有固定的规范。每个数据库都会有不同的实现方式。
pom.xml
引入依赖项<dependencies>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>4.2.3version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-simpleartifactId>
<version>1.7.25version>
<scope>compilescope>
dependency>
<dependency>
<groupId>javax.servlet.jspgroupId>
<artifactId>jsp-apiartifactId>
<version>2.2version>
<scope>providedscope>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>jstlartifactId>
<version>1.2version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>4.0.1version>
<scope>compilescope>
dependency>
dependencies>
spring-06-jedis
,设置模块500
错误)index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SecKilltitle>
<link crossorigin="anonymous" href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css" rel="stylesheet">
head>
<body>
<h1>iPhone 14 Pro 一元秒杀!!!h1>
<form id="msform" method="post" action="${pageContext.request.contextPath}/doSecKill" enctype="application/x-www-form-urlencoded">
<input type="hidden" id="prodid" name="prodid" value="0101">
<input class="btn btn-info" type="button" id="secKill_btn" name="secKill_btn" value="点我抢"/>
form>
body>
<script crossorigin="anonymous" src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js">script>
<script type="text/javascript">
$(function () {
$("#secKill_btn").click(function (){
var url=$("#msform").attr("action");
$.post(url, $("#msform").serialize(), function (data) {
if (data == "false"){
alert("ops!! 抢完了");
$("#secKill_btn").attr("disabled", true);
}
});
})
})
script>
html>
SecKillServlet.java
package com.mao;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Random;
public class SecKillServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public SecKillServlet(){super();}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
String userid = new Random().nextInt(5000) + "";
String prodid = request.getParameter("prodid");
boolean isSuccess = SecKillRedis.doSecKill(userid, prodid);
response.getWriter().print(isSuccess);
}
}
SecKillRedis.java
package com.mao;
import redis.clients.jedis.Jedis;
public class SecKillRedis {
public static boolean doSecKill(String userid, String prodid){
//1. uid和prodid非空判断
if(userid == null || prodid == null){
return false;
}
//2. 连接redis
Jedis jedis = new Jedis("192.168.211.130", 6379);
jedis.auth("******");
//3. 拼接key
// 3.1 库存key
String kc_key = "sk:" + prodid + ":qt";
// 3.2 秒杀成功用户id
String user_key = "sk:" + prodid + ":user";
//4.获取库存,如果为null说明秒杀还未开始
String kc = jedis.get(kc_key);
if (kc == null){
System.out.println("秒杀还未开始");
jedis.close();
return false;
}
//5.用户是否重复秒杀
if (jedis.sismember(user_key, userid)){
System.out.println("已抢到,请勿重复!");
jedis.close();
return false;
}
//6.判断如果商品的库存数量小于1,秒杀结束
if (Integer.parseInt(kc) <= 0){
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
//7.秒杀过程
// 7.1 库存-1
jedis.decr(kc_key);
// 7.2 将秒杀成功的用户添加进清单里
jedis.sadd(user_key, userid);
System.out.println("已抢到一台!");
jedis.close();
return true;
}
}
web.xml
若未配置,点击按钮时会报404
错误
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>jedis_secKilldisplay-name>
<servlet>
<servlet-name>seckillservlet-name>
<servlet-class>com.mao.SecKillServletservlet-class>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>seckillservlet-name>
<url-pattern>/doSecKillurl-pattern>
servlet-mapping>
web-app>
ab(apache bench)是apache下的一个工具,主要用于做web站点的压力测试
扩展:Jmeter工具
sudo apt-get install apache2-utils
yum install httpd-tools
ab工具参数如下:
moonjay@vm-ubuntu:~/桌面$ ab -help
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 benchmarking
This implies -n 50000
-s timeout Seconds to max. wait for each response
Default 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 attributes
are a colon separated username and password.
-P attribute Add Basic Proxy Authentication, the attributes
are 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
-l Accept variable document length (use this for dynamic pages)
-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.
-m method Method name
-h Display usage information (this message)
-I Disable TLS Server Name Indication (SNI) extension
-Z ciphersuite Specify SSL/TLS cipher suite (See openssl ciphers)
-f protocol Specify SSL/TLS protocol
(SSL2, TLS1, TLS1.1, TLS1.2 or ALL)
-E certfile Specify optional client certificate chain and private key
运行ab -n 100 -c 10 URL
,对URL进行100次请求,10个并发请求压力测试结果。
对上节的秒杀案例进行压力测试
ab测试命令
ab -n 100 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://[Tomcat sever ip]:8080/doSecKill
节省每次连接redis服务带来的消耗,把连接好的实例反复利用,通过参数管理连接的行为。
代码如下(单例模式实现):
package com.mao;
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil(){}
public static JedisPool getJedisPoolInstance(){
if(null == jedisPool){
synchronized (JedisPoolUtil.class){
if (jedisPool == null){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(200); // 控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;
// 如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
jedisPoolConfig.setMaxIdle(32); // 控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
jedisPoolConfig.setBlockWhenExhausted(true);
jedisPoolConfig.setTestOnBorrow(true); // 获得一个jedis实例的时候是否检查连接可用性( ping() );如果为true,则得到的jedis实例均是可用的;
jedisPool = new JedisPool(jedisPoolConfig, "192.168.211.130", 6379, 60000, "******")
}
}
}
return jedisPool;
}
public static void relese(JedisPool jedisPool, Jedis jedis){
if (null != jedis){
jedisPool.returnResource(jedis);
}
}
}
然后在SecKillRedis.java
中修改连接Redis使用连接池即可
// 使用连接池
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPool.getResource();
// 监视库存(乐观锁),在声明kc_key之后
jedis.watch(kc_key);
......
//7.秒杀过程
// 增加事务(multi)
Transaction transaction = jedis.multi();
// 7.1 库存-1
transaction.decr(kc_key); //jedis.decr(kc_key);
// 7.2 将秒杀成功的用户添加进清单里
transaction.sadd(user_key, userid); //jedis.sadd(user_key, userid);
// 执行事务
List<Object> results = transaction.exec();
if (results == null || results.size() == 0){
System.out.println("抢购失败了!");
jedis.close();
return false;
}
System.out.println("已抢到一台!");
jedis.close();
return true;
再次使用ab测试命令
ab -n 100 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://[Tomcat sever ip]:8080/doSecKill
可以在Redis客户端使用get sk:0101:qt
查看库存不为负数,而是0
上一篇文章:【学习笔记】尚硅谷-Redis6 1.Redis入门及数据类型介绍
下一篇文章:…