愿景:"让编程不再难学,让技术与生活更加有趣" 更多架构课程请访问 xdclass.net
简介:讲解全方位Redis6.x之战专题课程介绍,课程适合人员和学后水平
课程介绍
课程技术技术栈和测试环境说明
学后水平
学习形式
简介:讲解全方位Redis6.x之战课程大纲
课程学前基础
目录大纲浏览
学习要求:
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:高并发的必备两大“核技术”队列和缓存介绍
什么是队列(MQ消息中间件)
全称MessageQueue,主要是用于程序和程序直接通信,异步+解耦
使用场景:
核心应用
什么是缓存
简介:介绍本地缓存和分布式缓存
分布式缓存
本地缓存
和业务程序一起的缓存,例如myabtis的一级或者二级 缓存,本地缓存自然是最快的,但是不能在多个节点共 享
常⻅的本地缓存
选择本地缓存和分布式缓存
和业务数据结合去选择 高并发项目里面一般都是有本地缓存和分布式缓存共同 存在的
热点key的解决方案之一:避免带宽或者传输影响,本地缓存热点key数据,对于每次读请求,将首先检查key是否存在于本地缓存中,如果存在则直接返回,如果不存在再去访问分布式缓存的机器
简介:Nosql介绍和Reidis介绍
什么是Redis
属于NoSQL的一种 ( Not Only SQL )
官网地址:https://redis.io/
一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API
高性能:Redis能读的速度是110000次/s,写的速度是81000次/s
内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多 种类型的数据结构,如 字符串(strings)、散列(hashes)、 列表(lists)、 集合(sets)、 有序集合(sorted sets)等
谁在使用Redis
国外: Google、Facebook、亚马逊
国内:阿里、腾讯、字节、百度
高级工程师岗位面试都喜欢问Redis
简介:阿里云Linux服务器购买和常用软件介绍
云厂商
阿里云新用户地址(如果地址失效,联系我或者客服即可,1折)
环境问题说明
务必使用CentOS 7 以上版本,64位系统,不要在Windows系统操作!!!!
尽量前面先使用阿里云部署
大家本地用虚拟机记得也要CentOS 7.x系统
vmware
注意:谁都不能保证每个人-硬件组成-系统版本-虚拟机软件版本都一样
windows工具 putty,xshell, security CRT
苹果系统MAC : 通过终端登录
linux图形操作工具(用于远程连接上传文件)
mac: filezilla
windows: winscp
参考资料:https://jingyan.baidu.com/article/ed2a5d1f346fd409f6be179a.html
简介:Linux服务器源码安装Redis6和相关依赖
x#安装gcc
yum install -y gcc-c++ autoconf automake
#centos7 默认的 gcc 默认是4.8.5,版本小于 5.3 无法编译,需要先安装gcc新版才能编译
gcc -v
#升级新版gcc,配置永久生效
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
scl enable devtoolset-9 bash
echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile
#编译redis
cd redis
make
#安装到指定目录
mkdir -p /usr/local/redis
make PREFIX=/usr/local/redis install
安装编译redis6需要升级gcc,默认自带的gcc版本比较老
目录介绍
简介:Linux服务器Docker安装+容器化部署Redis6
xxxxxxxxxx
云计算+容器化是当下的主流,也是未来的趋势, docker就是可以快速部署启动应用
实现虚拟化,完整资源隔离,一次编写,四处运行
但有一定的限制,比如Docker是基于Linux 64bit的,无法在32bit的linux/Windows/unix环境下使用
xxxxxxxxxx
* Docker安装
xxxxxxxxxx
安装并运行Docker。
yum install docker-io -y
systemctl start docker
检查安装结果。
docker info
启动使用Docker
systemctl start docker #运行Docker守护进程
systemctl stop docker #停止Docker守护进程
systemctl restart docker #重启Docker守护进程
docker ps查看容器
docker stop 容器id
修改镜像仓库
vim /etc/docker/daemon.json
#改为下面内容,然后重启docker
{
"debug":true,"experimental":true,
"registry-mirrors":["https://pb5bklzr.mirror.aliyuncs.com","https://hub-mirror.c.163.com","https://docker.mirrors.ustc.edu.cn"]
}
#查看信息
docker info
xxxxxxxxxx
如果访问不了,记得看防火墙/网络安全组端口是否开放
源码安装redis的话默认不能远程访问 docker安装redis可以远程访问
docker run -itd --name xdclass-redis -p 6379:6379 redis --requirepass 123456
-i 以交互模式运行容器,通常与
-t 同时使用;
-d 后台运行容器,并返回容器ID;
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:分布式缓存Redis6常见核心配置讲解
你现在必须要知道的配置
创建目录
创建自定义配置文件 (使用自带的也行)
xxxxxxxxxx
#任何ip可以访问
bind 0.0.0.0
#守护进程
daemonize yes
#密码
requirepass 123456
#日志文件
logfile "/usr/local/redis/log/redis.log"
#持久化文件名称
dbfilename xdclass.rdb
#持久化文件存储路径
dir /usr/local/redis/data
#持久化策略, 10秒内有个1个key改动,执行快照
save 10 1
启动redis指定配置文件
xxxxxxxxxx
./redis-server ../conf/redis.conf
核心是防止机器被入侵挖矿
简介:redis6可视化客户端安装使用实战+key命名规范
Linux服务器
云服务器开放网络安全组
本地虚拟机记得关闭防火墙
key命名规范
方便管理+易读
不要过长,本身key也占据空间
冒号分割,不要有特殊字符(空格-引号-转义符)
例子:业务名:表名:ID
单机默认16个数据库,集群的话则没有这个概念,而是solt槽位
简介:懒人学习redis6在线工具介绍
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:Redis6常见数据结构概览
简介:Redis6数据结构之String类型介绍和应用场景
简介:Redis6数据结构之List类型介绍和应用场景
数据结构介绍:双向链表,插入删除时间复杂度O(1)快,查找为O(n)慢
应用场景:简单队列、最新商品列表、评论列表
简介:Redis6数据结构之List核心命令讲解
简介:Redis6数据结构之Hash类型介绍和应用场景
数据结构介绍:
应用场景:购物车存储、用户对象
简介:Redis6数据结构之Hash核心命令介绍
简介:Redis6数据结构之Set类型介绍和应用场景
数学:差集、交集、并集
数据结构介绍:
应用场景:大数据里面的用户画像标签集合、社交应用里面的共同好有
简介:Redis6数据结构之SortedSet类型介绍和应用场景
简介:Redis6数据结构之SortedSet核心命令介绍
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:分布式缓存Redis客户端讲解
自带客户端 redis-cli
可视化工具
语言客户端:java、nodejs、python
java语言客户端:
xxxxxxxxxx
Jedis 是直连模式,在多个线程间共享一个 Jedis 实例时是线程不安全的,需要使用连接池
其API提供了比较全面的Redis命令的支持,相比于其他Redis 封装框架更加原生
Jedis中的方法调用是比较底层的暴露的Redis的API,Java方法基本和Redis的API保持着一致
使用阻塞的I/O,方法调用同步,程序流需要等到socket处理完I/O才能执行,不支持异步操作
xxxxxxxxxx
高级Redis客户端,用于线程安全同步,异步响应
基于Netty的的事件驱动,可以在多个线程间并发访问, 通过异步的方式可以更好的利用系统资源
简介:新版SpringBoot2.X项目创建
新版SpringBoot2.X介绍
相关软件环境和作用
JDK1.8+以上
Maven3.5+
编辑器IDEA(旗舰版)
PostMan
翻译神器
在线创建 :https://start.spring.io/
注意:
采用springboot2.5 + jdk11
初次导入项目下载包比较慢 5~20分钟不等
不建议修改默认maven仓库(可以先还原默认的,防止下载包失败)
idea记得配置jdk11
简介:SpringBoot2.x整合Redis客户端+单元测试
在SpringBoot整合Redis很简单
SpringData介绍
添加依赖 spring-boot-starter-data-redis
xxxxxxxxxx
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
注意
Springboot2后默认使用Lettuce作为访问redis的客户端
旧版本lettuce存在堆外内存溢出的bug, 5.3版本修复了这个bug, 我们是用 6.1
很多同学没产生原因
解决方式
简介:SpringDataRedis配置RedisTemplate介绍
RedisTemplate介绍
RedisTemplate和StringRedisTemplate的区别
StringRedisTemplate继承RedisTemplate
两者的数据是不共通的(默认的序列化机制导致key不一样)
StringRedisTemplate默认采用的是String的序列化策略
RedisTemplate默认采用的是JDK的序列化策略,会将数据先序列化成字节数组然后在存入Redis数据库
总结
操作
String结构
简介:RedisTemplate的序列和反序列化机制讲解
上集问题
什么是序列化
把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途
Redis为什么要序列化
性能可以提高,不同的序列化方式性能不一样
可视化工具更好查看
自定义redis序列化方式,提供了多种可选择策略
JdkSerializationRedisSerializer
StringRedisSerializer
Jackson2JsonRedisSerializer
GenericFastJsonRedisSerializer
...
简介:自定义序列化和反序列化机制配置实战
xxxxxxxxxx
@Configuration
public class RedisTemplateConfiguration {
/**
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置key和value的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// 设置hashKey和hashValue的序列化规则
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 设置支持事物
//redisTemplate.setEnableTransactionSupport(true);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
简介:SpringBoot整合Jedis+Lettuce客户端连接池配置实战
基于SpringDataRedis可以快速替换底层实现
xxxxxxxxxx
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
xxxxxxxxxx
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active = 10
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle = 10
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle = 0
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait= -1ms
#指定客户端
spring.redis.client-type = lettuce
xxxxxxxxxx
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--不用指定版本号,本身spring-data-redis里面有-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.1</version>
</dependency>
xxxxxxxxxx
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active = 10
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle = 10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle = 0
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait= -1ms
#指定客户端
spring.redis.client-type = jedis
断点调试 redisTemplate的connectionFactory实现
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:案例实战之注册登录-图形验证码+谷歌开源Kaptcha引入
背景
注册-登录-修改密码一般需要发送验证码,但是容易被攻击恶意调用
什么是短信-邮箱轰炸机
公司带来的损失
如何避免自己的网站成为”肉鸡“或者被刷呢
Kaptcha 框架介绍
谷歌开源的一个可高度配置的实用验证 码生成工具
添加依赖
xxxxxxxxxx
<!--kaptcha依赖包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kaptcha-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
xxxxxxxxxx
@Configuration
public class CaptchaConfig {
/**
* 验证码配置
* Kaptcha配置类名
*
* @return
*/
@Bean
@Qualifier("captchaProducer")
public DefaultKaptcha kaptcha() {
DefaultKaptcha kaptcha = new DefaultKaptcha();
Properties properties = new Properties();
//验证码个数
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
//字体间隔
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,"8");
//干扰线颜色
//干扰实现类
properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
//图片样式
properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");
//文字来源
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789");
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
}
简介:验证码存储Redis逻辑编码实战
xxxxxxxxxx
/**
* 获取ip
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
return ipAddress;
}
public static String MD5(String data) {
try {
java.security.MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
} catch (Exception exception) {
}
return null;
}
xxxxxxxxxx
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private Producer captchaProducer;
/**
*临时使用10分钟有效,方便测试
*/
private static final long CAPTCHA_CODE_EXPIRED = 60 * 1000 * 10;
/**
* 获取图形验证码
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response){
String captchaText = captchaProducer.createText();
//存储
redisTemplate.opsForValue().set(getCaptchaKey(request),captchaText,CAPTCHA_CODE_EXPIRED,TimeUnit.MILLISECONDS);
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
ImageIO.write(bufferedImage,"jpg",outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
}
}
/**
* 获取缓存的key
* @param request
* @return
*/
private String getCaptchaKey(HttpServletRequest request){
String ip = CommonUtils.getIpAddr(request);
String userAgent = request.getHeader("User-Agent");
String key = "user-service:captcha:"+CommonUtils.MD5(ip+userAgent);
return key;
}
简介:JsonData工具类封装+验证码校验编码实战
xxxxxxxxxx
public class JsonData {
/**
* 状态码 0 表示成功
*/
private Integer code;
/**
* 数据
*/
private Object data;
/**
* 描述
*/
private String msg;
public JsonData(int code,Object data,String msg){
this.code = code;
this.msg = msg;
this.code = code;
}
/**
* 成功,不传入数据
* @return
*/
public static JsonData buildSuccess() {
return new JsonData(0, null, null);
}
/**
* 成功,传入数据
* @param data
* @return
*/
public static JsonData buildSuccess(Object data) {
return new JsonData(0, data, null);
}
/**
* 失败,传入描述信息
* @param msg
* @return
*/
public static JsonData buildError(String msg) {
return new JsonData(-1, null, msg);
}
//set get 方法省略
}
xxxxxxxxxx
/**
* 支持手机号、邮箱发送验证码
* @return
*/
@GetMapping("send_code")
public JsonData sendRegisterCode(@RequestParam(value = "to", required = true)String to,
@RequestParam(value = "captcha", required = true)String captcha,
HttpServletRequest request){
String key = getCaptchaKey(request);
String cacheCaptcha = redisTemplate.opsForValue().get(key);
if(captcha!=null && cacheCaptcha!=null && cacheCaptcha.equalsIgnoreCase(captcha)) {
redisTemplate.delete(key);
//TODO 发送验证码
return jsonData;
}else {
return JsonData.buildResult("图形验证码错误");
}
}
简介:高并发商品首页热点数据开发实战
热点数据
链路逻辑
接口开发
注意点
简介: 一线大厂必备Jmeter5.x压力测试工具急速入门
LoadRunner
Apache AB(单接口压测最方便)
Webbench
Jmeter
压测工具本地快速安装Jmeter5.x
目录
xxxxxxxxxx
bin:核心可执行文件,包含配置
jmeter.bat: windows启动文件(window系统一定要配置显示文件拓展名)
jmeter: mac或者linux启动文件
jmeter-server:mac或者Liunx分布式压测使用的启动文件
jmeter-server.bat:window分布式压测使用的启动文件
jmeter.properties: 核心配置文件
extras:插件拓展的包
lib:核心的依赖包
Jmeter语言版本中英文切换
配置文件修改
简介:讲解Jmeter里面GUI菜单栏主要组件
添加->threads->线程组(控制总体并发)
xxxxxxxxxx
线程数:虚拟用户数。一个虚拟用户占用一个进程或线程
准备时长(Ramp-Up Period(in seconds)):全部线程启动的时长,比如100个线程,20秒,则表示20秒内 100个线程都要启动完成,每秒启动5个线程
循环次数:每个线程发送的次数,假如值为5,100个线程,则会发送500次请求,可以勾选永远循环
线程组->添加-> Sampler(采样器) -> Http (一个线程组下面可以增加几个Sampler)
xxxxxxxxxx
名称:采样器名称
注释:对这个采样器的描述
web服务器:
默认协议是http
默认端口是80
服务器名称或IP :请求的目标服务器名称或IP地址
路径:服务器URL
查看测试结果
xxxxxxxxxx
线程组->添加->监听器->察看结果树
线程组->添加->监听器->聚合报告
简介: Jmeter5.x压测接口实战-接口性能优化前后QPS对比
热点数据接口压测
新增聚合报告:线程组->添加->监听器->聚合报告(Aggregate Report)
xxxxxxxxxx
lable: sampler的名称
Samples: 一共发出去多少请求,例如10个用户,循环10次,则是 100
Average: 平均响应时间
Median: 中位数,也就是 50% 用户的响应时间
90% Line : 90% 用户的响应不会超过该时间 (90% of the samples took no more than this time. The remaining samples at least as long as this)
95% Line : 95% 用户的响应不会超过该时间
99% Line : 99% 用户的响应不会超过该时间
min : 最小响应时间
max : 最大响应时间
Error%:错误的请求的数量/请求的总数
Throughput: 吞吐量——默认情况下表示每秒完成的请求数(Request per Second) 可类比为qps、tps
KB/Sec: 每秒接收数据量
基于当前机器配置压测
当前架构存在的问题
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:分布式锁核心知识介绍和注意事项
背景
就是保证同一时间只有一个客户端可以对共享资源进行操作
案例:优惠券领劵限制张数、商品库存超卖
核心
避免共享资源并发操作导致数据问题
加锁
设计分布式锁应该考虑的东西
排他性
容错性
满足可重入、高性能、高可用
注意分布式锁的开销、锁粒度
简介:基于Redis实现分布式锁的几种坑
实现分布式锁 可以用 Redis、Zookeeper、Mysql数据库这几种 , 性能最好的是Redis且是最容易理解
xxxxxxxxxx
key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种优惠券活动加锁,key 命名为 “coupon:id” 。value就可以使用固定值,比如设置成1
基于redis实现分布式锁,文档:http://www.redis.cn/commands.html#string
xxxxxxxxxx
setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作
如果 key 不存在,则设置当前 key 成功,返回 1;
如果当前 key 已经存在,则设置当前 key 失败,返回 0
xxxxxxxxxx
得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)
xxxxxxxxxx
客户端奔溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放
xxxxxxxxxx
methodA(){
String key = "coupon_66"
if(setnx(key,1) == 1){
expire(key,30,TimeUnit.MILLISECONDS)
try {
//做对应的业务逻辑
//查询用户是否已经领券
//如果没有则扣减库存
//新增领劵记录
} finally {
del(key)
}
}else{
//睡眠100毫秒,然后自旋调用本方法
methodA()
}
}
简介:手把手教你彻底掌握分布式锁+原生代码编写
存在什么问题?
setnx
和expire
之间,如果setnx
成功,但是expire
失败,且宕机了,则这个资源就是死锁xxxxxxxxxx
使用原子命令:设置和配置过期时间 setnx / setex
如: set key 1 ex 30 nx
java里面 redisTemplate.opsForValue().setIfAbsent("seckill_1","success",30,TimeUnit.MILLISECONDS)
xxxxxxxxxx
可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁, 那 value 应该是存当前线程的标识或者uuid
String key = "coupon_66"
String value = Thread.currentThread().getId()
if(setnx(key,value) == 1){
expire(key,30,TimeUnit.MILLISECONDS)
try {
//做对应的业务逻辑
} finally {
//删除锁,判断是否是当前线程加的
if(get(key).equals(value)){
//还存在时间间隔
del(key)
}
}
}else{
//睡眠100毫秒,然后自旋调用本方法
}
进一步细化误删
总结
简介:手把手教你彻底掌握分布式锁+原生代码编写
前面说了redis做分布式锁存在的问题
xxxxxxxxxx
//获取lock的值和传递的值一样,调用删除操作返回1,否则返回0
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//Arrays.asList(lockKey)是key列表,uuid是参数
Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);
xxxxxxxxxx
/**
* 原生分布式锁 开始
* 1、原子加锁 设置过期时间,防止宕机死锁
* 2、原子解锁:需要判断是不是自己的锁
*/
@RestController
@RequestMapping("/api/v1/coupon")
public class CouponController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("add")
public JsonData saveCoupon(@RequestParam(value = "coupon_id",required = true) int couponId){
//防止其他线程误删
String uuid = UUID.randomUUID().toString();
String lockKey = "lock:coupon:"+couponId;
lock(couponId,uuid,lockKey);
return JsonData.buildSuccess();
}
private void lock(int couponId,String uuid,String lockKey){
//lua脚本
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30));
System.out.println(uuid+"加锁状态:"+nativeLock);
if(nativeLock){
//加锁成功
try{
//TODO 做相关业务逻辑
TimeUnit.SECONDS.sleep(10L);
} catch (InterruptedException e) {
} finally {
//解锁
Long result = redisTemplate.execute( new DefaultRedisScript<>(script,Long.class),Arrays.asList(lockKey),uuid);
System.out.println("解锁状态:"+result);
}
}else {
//自旋操作
try {
System.out.println("加锁失败,睡眠5秒 进行自旋");
TimeUnit.MILLISECONDS.sleep(5000);
} catch (InterruptedException e) { }
//睡眠一会再尝试获取锁
lock(couponId,uuid,lockKey);
}
}
}
遗留一个问题,锁的过期时间,如何实现锁的自动续期 或者 避免业务执行时间过长,锁过期了?
原生代码+redis实现分布式锁使用比较复杂,且有些锁续期问题更难处理
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:在线教育-天热销视频榜单实战-List数据结构设计
需求
企业中流程
类似场景
疑惑:为啥不是实时计算,真正高并发下项目,都是预先计算好结果,然后直接返回数据,且存储结构最简单
简介:在线教育-天热销视频榜单实战-编码最佳实践
xxxxxxxxxx
@RequestMapping("rank")
public JsonData videoRank(){
List<VideoDO> list = redisTemplate.opsForList().range(RANK_KEY,0,-1);
return JsonData.buildSuccess(list);
}
xxxxxxxxxx
@Test
void saveRank(){
String RANK_KEY = "rank:video";
VideoDO video1 = new VideoDO(3,"PaaS工业级微服务大课","xdclass.net",1099);
VideoDO video2 = new VideoDO(5,"AlibabaCloud全家桶实战","xdclass.net",59);
VideoDO video3 = new VideoDO(53,"SpringBoot2.X+Vue3综合实战","xdclass.net",49);
VideoDO video4 = new VideoDO(15,"玩转23种设计模式+最近实战","xdclass.net",49);
VideoDO video5 = new VideoDO(45,"Nginx网关+LVS+KeepAlive","xdclass.net",89);
redisTemplate.opsForList().leftPushAll(RANK_KEY,video4,video5,video3,video2,video1);
}
xxxxxxxxxx
/**
* 替换榜单第二名
*/
@Test
void replaceRank(){
String RANK_KEY = "rank:video";
VideoDO video = new VideoDO(42,"小滴课堂面试专题第一季高级工程师","xdclass.net",89);
//在集合的指定位置插入元素,如果指定位置已有元素,则覆盖,没有则新增
redisTemplate.opsForList().set(RANK_KEY,1,video);
}
简介:自营电商平台-购物车实现案例-Hash数据结构最佳实践
背景
购物车常见实现方式
实现方式一:存储到数据库
实现方式二:前端本地存储-localstorage-sessionstorage
实现方式三:后端存储到缓存如redis
购物车数据结构介绍
一个购物车里面,存在多个购物项
所以 购物车结构是一个双层Map:
对应redis里面的存储
简介:高并发下的互联网电商购物车实战-相关VO类和数据准备
xxxxxxxxxx
public class CartItemVO {
/**
* 商品id
*/
private Integer productId;
/**
* 购买数量
*/
private Integer buyNum;
/**
* 商品标题
*/
private String productTitle;
/**
* 图片
*/
private String productImg;
/**
* 商品单价
*/
private int price ;
/**
* 总价格,单价+数量
*/
private int totalPrice;
public int getProductId() {
return productId;
}
public void setProductId(int productId) {
this.productId = productId;
}
public Integer getBuyNum() {
return buyNum;
}
public void setBuyNum(Integer buyNum) {
this.buyNum = buyNum;
}
public String getProductTitle() {
return productTitle;
}
public void setProductTitle(String productTitle) {
this.productTitle = productTitle;
}
public String getProductImg() {
return productImg;
}
public void setProductImg(String productImg) {
this.productImg = productImg;
}
/**
* 商品单价 * 购买数量
* @return
*/
public int getTotalPrice() {
return this.price*this.buyNum;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public void setTotalPrice(int totalPrice) {
this.totalPrice = totalPrice;
}
}
xxxxxxxxxx
public class CartVO {
/**
* 购物项
*/
private List<CartItemVO> cartItems;
/**
* 购物车总价格
*/
private Integer totalAmount;
/**
* 总价格
* @return
*/
public int getTotalAmount() {
return cartItems.stream().mapToInt(CartItemVO::getTotalPrice).sum();
}
public List<CartItemVO> getCartItems() {
return cartItems;
}
public void setCartItems(List<CartItemVO> cartItems) {
this.cartItems = cartItems;
}
}
xxxxxxxxxx
@Repository
public class VideoDao {
private static Map<Integer,VideoDO> map = new HashMap<>();
static {
map.put(1,new VideoDO(1,"工业级PaaS云平台+SpringCloudAlibaba 综合项目实战(完结)","https://xdclass.net",1099));
map.put(2,new VideoDO(2,"玩转新版高性能RabbitMQ容器化分布式集群实战","https://xdclass.net",79));
map.put(3,new VideoDO(3,"新版后端提效神器MybatisPlus+SwaggerUI3.X+Lombok","https://xdclass.net",49));
map.put(4,new VideoDO(4,"玩转Nginx分布式架构实战教程 零基础到高级","https://xdclass.net",49));
map.put(5,new VideoDO(5,"ssm新版SpringBoot2.3/spring5/mybatis3","https://xdclass.net",49));
map.put(6,new VideoDO(6,"新一代微服务全家桶AlibabaCloud+SpringCloud实战","https://xdclass.net",59));
}
/**
* 模拟从数据库找
* @param videoId
* @return
*/
public VideoDO findDetailById(int videoId) {
return map.get(videoId);
}
}
xxxxxxxxxx
public class JsonUtil {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 将对象转换成json字符串。
* @return
*/
public static String objectToJson(Object data) {
try {
String string = MAPPER.writeValueAsString(data);
return string;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转化为对象
*
* @param jsonData json数据
* @param clazz 对象中的object类型
* @return
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
简介:电商购物车实现案例-加入购物车接口开发
xxxxxxxxxx
@RequestMapping("addCart")
public JsonData addCart(int videoId,int buyNum){
//获取购物车
BoundHashOperations<String, Object, Object> myCart = getMyCartOps();
Object cacheObj = myCart.get(videoId+"");
String result = "";
if (cacheObj != null) {
result = (String) cacheObj;
}
if (cacheObj == null) {
//不存在则新建一个购物项
CartItemVO cartItem = new CartItemVO();
//从数据库查询详情,我们这边直接随机写个
VideoDO videoDO = videoDao.findDetailById(videoId);
videoDO.setId(videoId);
cartItem.setPrice(videoDO.getPrice());
cartItem.setBuyNum(buyNum);
cartItem.setProductId(videoId);
cartItem.setProductImg(videoDO.getImg());
cartItem.setProductTitle(videoDO.getTitle());
myCart.put(videoId+"", JsonUtil.objectToJson(cartItem));
} else {
//存在则新增数量
CartItemVO cartItem = JsonUtil.jsonToPojo(result, CartItemVO.class);
cartItem.setBuyNum(cartItem.getBuyNum() + buyNum);
myCart.put(videoId+"", JsonUtil.objectToJson(cartItem));
}
return JsonData.buildSuccess();
}
xxxxxxxxxx
/**
* 抽取我的购物车通用方法
*
* @return
*/
private BoundHashOperations<String, Object, Object> getMyCartOps() {
String cartKey = getCartKey();
return redisTemplate.boundHashOps(cartKey);
}
/**
* 获取购物车的key
*
* @return
*/
private String getCartKey() {
//从拦截器获取 ,这里写死即可,每个用户不一样
int userId = 88;
String cartKey = String.format("product:cart:%s", userId);
return cartKey;
}
简介:高并发下的互联网电商购物车实战-查看和清空购物车功能开发
xxxxxxxxxx
@GetMapping("/mycart")
public JsonData findMyCart(){
BoundHashOperations<String,Object,Object> myCart = getMyCartOps();
List<Object> itemList = myCart.values();
List<CartItemVO> cartItemVOList = new ArrayList<>();
for(Object item: itemList){
CartItemVO cartItemVO = JsonUtil.jsonToPojo((String)item,CartItemVO.class);
cartItemVOList.add(cartItemVO);
}
//封装成cartvo
CartVO cartVO = new CartVO();
cartVO.setCartItems(cartItemVOList);
return JsonData.buildSuccess(cartVO);
}
xxxxxxxxxx
@GetMapping("/clear")
public JsonData clear() {
String cartKey = getCartKey();
redisTemplate.delete(cartKey);
return JsonData.buildSuccess();
}
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:案例实战需求之大数据下的用户画像标签去重
介绍
案例
xxxxxxxxxx
BoundSetOperations operations = redisTemplate.boundSetOps("user:tags:1");
operations.add("car","student","rich","guangdong","dog","rich");
Set<String> set1 = operations.members();
System.out.println(set1);
operations.remove("dog");
Set<String> set2 = operations.members();
System.out.println(set2);
return JsonData.buildSuccess();
简介:案例实战社交应用里面之关注、粉丝、共同好友案例
背景
案例实战
xxxxxxxxxx
public void testSet(){
BoundSetOperations operationLW = redisTemplate.boundSetOps("user:lw");
operationLW.add("A","B","C","D","E");
System.out.println("老王的粉丝:"+operationLW.members());
BoundSetOperations operationXD = redisTemplate.boundSetOps("user:xd");
operationXD.add("A","B","F","G","H","J");
System.out.println("小D的粉丝:"+operationXD.members());
//差集
Set lwSet = operationLW.diff("user:xd");
System.out.println("老王的优势:"+lwSet);
//差集
Set xdSet = operationXD.diff("user:lw");
System.out.println("小滴的优势:"+xdSet);
//交集
Set interSet = operationLW.intersect("user:xd");
System.out.println("共同好友:"+interSet);
//并集
Set unionSet = operationLW.union("user:xd");
System.out.println("两个人的并集:"+unionSet);
//用户A是否是 老王 的粉丝
boolean flag = operationLW.isMember("A");
System.out.println(flag);
}
简介:案例实战之SortedSet用户积分实时榜单最佳实践
背景
对象准备
xxxxxxxxxx
public class UserPointVO {
public UserPointVO(String username, String phone) {
this.username = username;
this.phone = phone;
}
private String username;
private String phone;
}
xxxxxxxxxx
@Test
void testData() {
UserPoint p1 = new UserPoint("老王","13113");
UserPoint p2 = new UserPoint("老A","324");
UserPoint p3 = new UserPoint("老B","242");
UserPoint p4 = new UserPoint("老C","542345");
UserPoint p5 = new UserPoint("老D","235");
UserPoint p6 = new UserPoint("老E","1245");
UserPoint p7 = new UserPoint("老F","2356432");
UserPoint p8 = new UserPoint("老G","532332");
BoundZSetOperations<String, UserPoint> operations = redisTemplate.boundZSetOps("point:rank:real");
operations.add(p1,888);
...
}
接口开发
简介:案例实战之SortedSet用户积分实时榜单多接口实战
多接口实战
接口开发
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:SpringCache缓存框架介绍
SpringCache简介
自Spring 3.1起,提供了类似于@Transactional注解事务的注解Cache支持,且提供了Cache抽象
提供基本的Cache抽象,方便切换各种底层Cache
只需要更少的代码就可以完成业务数据的缓存
提供事务回滚时也自动回滚缓存,支持比较复杂的缓存逻辑
核心
讲课方式
使用
xxxxxxxxxx
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
xxxxxxxxxx
spring:
cache:
type: redis
xxxxxxxxxx
@EnableCaching
简介:整合MybatisPlus连接Mysql数据库
备注:
添加依赖
xxxxxxxxxx
<!--mybatis plus和springboot整合-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
xxxxxxxxxx
#==============================数据库相关配置========================================
#数据库配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/xdclass_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: xdclass.net
#配置plus打印sql日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
xxxxxxxxxx
CREATE TABLE `product` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(128) DEFAULT NULL COMMENT '标题',
`cover_img` varchar(128) DEFAULT NULL COMMENT '封面图',
`detail` varchar(256) DEFAULT '' COMMENT '详情',
`amount` int(10) DEFAULT NULL COMMENT '新价格',
`stock` int(11) DEFAULT NULL COMMENT '库存',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
xxxxxxxxxx
INSERT INTO `product` (`id`, `title`, `cover_img`, `detail`, `amount`, `stock`, `create_time`)
VALUES
(1, '老王-小滴课堂抱枕', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', 'https://file.xdclass.net/video/2021/60-MLS/summary.jpeg', 213, 100, '2021-09-12 00:00:00'),
(2, '老王-技术人的杯子Linux', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', 'https://file.xdclass.net/video/2021/59-Postman/summary.jpeg', 42, 100, '2021-03-12 00:00:00'),
(3, '技术人的杯子docker', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', 'https://file.xdclass.net/video/2021/60-MLS/summary.jpeg', 12, 20, '2022-09-22 00:00:00'),
(4, '技术人的杯子git', 'https://file.xdclass.net/video/2020/alibabacloud/zt-alibabacloud.png', 'https://file.xdclass.net/video/2021/60-MLS/summary.jpeg', 14, 20, '2022-11-12 00:00:00');
xxxxxxxxxx
@TableName("product")
public class ProductDO {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 标题
*/
private String title;
/**
* 封面图
*/
private String coverImg;
/**
* 详情
*/
private String detail;
/**
* 新价格
*/
private Integer amount;
/**
* 库存
*/
private Integer stock;
/**
* 创建时间
*/
private Date createTime;
}
简介: SpringBoot+MybatisPlus开发商品列表的CRUD接口
xxxxxxxxxx
public interface ProductMapper extends BaseMapper<ProductDO> {
}
xxxxxxxxxx
@Override
public int save(ProductDO productDO) {
return productMapper.insert(productDO);
}
@Override
public int delById(int id) {
return productMapper.deleteById(id);
}
@Override
public int updateById(ProductDO productDO) {
return productMapper.updateById(productDO);
}
@Override
public ProductDO findById(int id) {
return productMapper.selectById(id);
}
简介: SpringBoot+MybatisPlus开发商品分页接口
为啥要开发分页接口?
MybatisPlus分页接口
xxxxxxxxxx
@Override
public Map<String, Object> page(int page, int size) {
Page<ProductDO> pageInfo = new Page<>(page, size);
IPage<ProductDO> productDOIPage = productMapper.selectPage(pageInfo, null);
Map<String, Object> pageMap = new HashMap<>(3);
pageMap.put("total_record", productDOIPage.getTotal());
pageMap.put("total_page", productDOIPage.getPages());
pageMap.put("current_data", productDOIPage.getRecords());
return pageMap;
}
xxxxxxxxxx
/**
* 新的分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:SpringCache框架常用注解Cacheable
Cacheable注解
xxxxxxxxxx
//对象
@Cacheable(value = {"product"}, key="#root.methodName")
//分页
@Cacheable(value = {"product_page"},key="#root.methodName + #page+'_'+#size")
spEL表达式
methodName 当前被调用的方法名
args 当前被调用的方法的参数列表
result 方法执行后的返回值
简介:SpringCache框架自定义CacheManager配置和过期时间
xxxxxxxxxx
@Bean
@Primary
public RedisCacheManager cacheManager1Hour(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = instanceConfig(3600L);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
@Bean
public RedisCacheManager cacheManager1Day(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = instanceConfig(3600 * 24L);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
private RedisCacheConfiguration instanceConfig(Long ttl) {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
// 去掉各种@JsonSerialize注解的解析
objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false);
// 只针对非空的值进行序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 将类型序列化到属性json字符串中
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(ttl))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
}
简介:SpringCache框架自定义缓存KeyGenerator
xxxxxxxxxx
@Bean
public KeyGenerator springCacheDefaultKeyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... objects) {
return o.getClass().getSimpleName() + "_"
+ method.getName() + "_"
+ StringUtils.arrayToDelimitedString(objects, "_");
}
};
}
使用
xxxxxxxxxx
@Cacheable(value = {"product"},keyGenerator = "springCacheCustomKeyGenerator", cacheManager = "cacheManager1Minute")
简介:SpringCache框架常用注解CachePut
CachePut
案例实战
xxxxxxxxxx
@CachePut(value = {"product"},key = "#productDO.id")
简介:SpringCache框架常用注解CacheEvict
CacheEvict
从缓存中移除相应数据, 触发缓存删除的操作
value 缓存名称,可以有多个
key 缓存的key规则,可以用springEL表达式,默认是方法参数组合
beforeInvocation = false
beforeInvocation = true
案例实战
xxxxxxxxxx
@CacheEvict(value = {"product"},key = "#root.args[0]")
简介:SpringCache框架多注解组合Caching
Caching
实战
xxxxxxxxxx
@Caching(
cacheable = {
@Cacheable(value = "product",keyGenerator = "xdclassKeyGenerator")
},
put = {
@CachePut(value = "product",key = "#id"),
@CachePut(value = "product",key = "'stock:'+#id")
}
)
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:分布式缓存必考题之缓存击穿+解决方案
缓存击穿 (某个热点key缓存失效了)
预防
SpringCache解决方案
xxxxxxxxxx
@Cacheable(value = {"product"},key = "#root.args[0]", cacheManager = "customCacheManager", sync=true)
简介:分布式缓存必考题之缓存雪崩+解决方案
缓存雪崩 (多个热点key都过期)
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩
预防
SpringCache解决方案
xxxxxxxxxx
cache:
#使用的缓存类型
type: redis
#过时时间
redis:
time-to-live: 3600000
# 开启前缀,默以为true
use-key-prefix: true
# 键的前缀,默认就是缓存名cacheNames
key-prefix: XD_CACHE
# 是否缓存空结果,防止缓存穿透,默以为true
cache-null-values: true
简介:分布式缓存必考题之缓存穿透+解决方案
缓存穿透(查询不存在数据)
查询一个不存在的数据,由于缓存是不命中的,并且出于容错考虑,如发起为id为“-1”不存在的数据
如果从存储层查不到数据则不写入缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。存在大量查询不存在的数据,可能DB就挂掉了,这也是黑客利用不存在的key频繁攻击应用的一种方式。
预防
SpringCache解决方案
xxxxxxxxxx
cache:
#使用的缓存类型
type: redis
#过时时间
redis:
time-to-live: 3600000
# 开启前缀,默以为true
use-key-prefix: true
# 键的前缀,默认就是缓存名cacheNames
key-prefix: XD_CACHE
# 是否缓存空结果,防止缓存穿透,默以为true
cache-null-values: true
简介:SpringCache整合SpringBoot案例总结
学会触类旁通,举一反三
企业中使用也是这样用的,根据项目和团队开发规范简单调整
给大家看一个案例代理:大课项目训练营中使用
工业级PaaS云平台+SpringCloudAlibaba 综合项目实战
学习地址:https://detail.tmall.com/item.htm?id=634842400121&skuId=4611246228920
基于2核4g,阿里云ECS通用性服务器
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介: Redis6.x持久化配置介绍和RDB讲解
Redis持久化介绍
两种持久化方式
RDB持久化介绍
在指定的时间间隔内将内存中的数据集快照写入磁盘
默认的文件名为dump.rdb
产生快照的情况
save
bgsave
自动化
主从架构
优点
缺点
核心配置
xxxxxxxxxx
#任何ip可以访问
bind 0.0.0.0
#守护进程
daemonize yes
#密码
requirepass 123456
#日志文件
logfile "/usr/local/redis/log/redis.log"
#持久化文件名称
dbfilename xdclass.rdb
#持久化文件存储路径
dir /usr/local/redis/data
#持久化策略, 10秒内有个1个key改动,执行快照
save 10 1
######之前配置######
#导出rdb数据库文件压缩字符串和对象,默认是yes,会浪费CPU但是节省空间
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
简介: 分布式缓存Redis6.x持久化配置RDB操作实战
配置持久化
save
bgsave
配置文件触发
xxxxxxxxxx
bind 0.0.0.0
daemonize yes
requirepass 123456Xdclass
logfile "/usr/local/redis/log/redis.log"
dbfilename xdclass.rdb
dir /usr/local/redis/data
#关闭rdb
#save ""
#10秒2个key变动则触发rdb
save 10 2
#100秒5个key变动则触发rdb
save 100 5
#压缩
rdbcompression yes
#检查
rdbchecksum yes
xxxxxxxxxx
0 表示内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程
1 表示内核允许分配所有的物理内存,而不管当前的内存状态如何。
2 表示内核允许分配超过所有物理内存和交换空间总和的内存
解决方式
echo 1 > /proc/sys/vm/overcommit_memory
持久化配置
vim /etc/sysctl.conf
改为
vm.overcommit_memory=1
修改sysctl.conf后,需要执行 sysctl -p 以使生效。
简介: Redis6.x持久化配置AOF介绍和配置实战
AOF持久化介绍
配置实战
核心原理
提供了3种同步方式,在性能和安全性方面做出平衡
appendfsync always
appendfsync everysec
appendfsync no
xxxxxxxxxx
bind 0.0.0.0
daemonize yes
requirepass 123456Xdclass
logfile "/usr/local/redis/log/redis.log"
dbfilename xdclass.rdb
dir /usr/local/redis/data
#save 10 2
#save 100 5
save ""
rdbcompression yes
#对rdb数据进行校验,耗费CPU资源,默认为yes
rdbchecksum yes
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
简介: Redis6.x持久化配置AOF重新rewrite配置实战
rewrite 重写介绍
重写触发配置
手动触发
自动触发
auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数
auto-aof-rewrite-min-size
auto-aof-rewrite-percentage
常用配置
xxxxxxxxxx
# 是否开启aof
appendonly yes
# 文件名称
appendfilename "appendonly.aof"
# 同步方式
appendfsync everysec
# aof重写期间是否同步
no-appendfsync-on-rewrite no
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 加载aof时如果有错如何处理
# yes表示如果aof尾部文件出问题,写log记录并继续执行。no表示提示写入等待修复后写入
aof-load-truncated yes
简介: Redis6.x持久化配置AOF和RDB的选择问题
Redis提供了不同的持久性选项:
xxxxxxxxxx
auto-aof-rewrite-min-size
AOF文件最小重写大小,只有当AOF文件大小大于该值时候才可能重写,6.x默认配置64mb。
auto-aof-rewrite-percentage
当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比,如100代表当前AOF文件是上次重写的两倍时候才重写。
RDB的优缺点
优点:
缺点:
AOF的优缺点
优点:
缺点:
在线上我们到底该怎么做?
Redis4.0后开始的rewrite支持混合模式
就是rdb和aof一起用
直接将rdb持久化的方式来操作将二进制内容覆盖到aof文件中,rdb是二进制,所以很小
有写入的话还是继续append追加到文件原始命令,等下次文件过大的时候再次rewrite
默认是开启状态
好处
坏处
数据恢复
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介: Redis6.x服务端配置info命令介绍
info命令介绍
xxxxxxxxxx
Server:有关redis服务器的常规信息
redis_mode:standalone # 运行模式,单机或者集群
multiplexing_api:epoll # redis所使用的事件处理机制
run_id:3abd26c33dfd059e87a0279defc4c96c13962ede # redis服务器的随机标识符(用于sentinel和集群)
config_file:/usr/local/redis/conf/redis.conf # 配置文件路径
Clients:客户端连接部分
connected_clients:10 # 已连接客户端的数量(不包括通过slave连接的客户端)
Memory:内存消耗相关信息
used_memory:874152 # 使用内存,以字节(byte)B为单位
used_memory_human:853.66K # 以人类可读的格式返回 Redis 分配的内存总量
used_memory_rss:2834432 # 系统给redis分配的内存即常驻内存,和top 、 ps 等命令的输出一致
used_memory_rss_human:2.70M # 以人类可读的格式返回系统redis分配的常驻内存top、ps等命令的输出一致
used_memory_peak:934040 # 内存使用的峰值大小
used_memory_peak_human:912.15K
total_system_memory:1039048704 # 操作系统的总内存 ,以字节(byte)为单位
total_system_memory_human:990.91M
used_memory_lua:37888 # lua引擎使用的内存
used_memory_lua_human:37.00K
maxmemory:0 # 最大内存的配置值,0是不限制
maxmemory_human:0B
maxmemory_policy:noeviction # 达到最大内存配置值后的策略
Persistence:RDB和AOF相关信息
rdb_bgsave_in_progress:0 # 标识rdb save是否进行中
rdb_last_bgsave_status:ok # 上次的save操作状态
rdb_last_bgsave_status:ok # 上次的save操作状态
rdb_last_bgsave_time_sec:-1 # 上次rdb save操作使用的时间(单位s)
rdb_current_bgsave_time_sec:-1 # 如果rdb save操作正在进行,则是所使用的时间
aof_enabled:1 # 是否开启aof,默认没开启
aof_rewrite_in_progress:0 # 标识aof的rewrite操作是否在进行中
aof_last_rewrite_time_sec:-1 # 上次rewrite操作使用的时间(单位s)
aof_current_rewrite_time_sec:-1 # 如果rewrite操作正在进行,则记录所使用的时间
aof_last_bgrewrite_status:ok # 上次rewrite操作的状态
aof_current_size:0 # aof当前大小
Stats:一般统计
evicted_keys:0 # 因为内存大小限制,而被驱逐出去的键的个数
Replication:主从同步信息
role:master # 角色
connected_slaves:1 # 连接的从库数
master_sync_in_progress:0 # 标识主redis正在同步到从redis
CPU:CPU消耗统计
Cluster:集群部分
cluster_enabled:0 # 实例是否启用集群模式
Keyspace:数据库相关统计
db0:keys=4,expires=0,avg_ttl=0 # db0的key的数量,带有生存期的key的数,平均存活时间
简介: Redis6.x服务端配置config命令介绍
config命令介绍(都有默认值)
常用配置
xxxxxxxxxx
daemonize #后端运行
bind #ip绑定
timeout #客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接
databases #设置数据库的个数,可以使用 SELECT 命令来切换数据库。默认使用的数据库是 0
save #设置 Redis 进行rdb持久化数据库镜像的频率。
rdbcompression #在进行镜像备份时,是否进行压缩
slaveof #设置该数据库为其他数据库的从数据库
masterauth #当主数据库连接需要密码验证时,在这里配置
maxclients #限制同时连接的客户数量,当连接数超过这个值时,redis 将不再接收其他连接请求,返回error
maxmemory #设置 redis 能够使用的最大内存,
maxmemory #设置 redis 能够使用的最大内存,
备注
注意:
简介:Redis6的key过期时间删除策略你知道多少
背景
redis key过期策略
定期删除+惰性删除。
Redis如何淘汰过期的keys: set name xdclass 3600
定期删除:
惰性删除 :
概念:当一些客户端尝试访问它时,key会被发现并主动的过期
放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键
问题
简介:内存不足时-Redis的Key内存淘汰策略你知道多少
背景
策略
volatile-lru(least recently used)
volatile-lfu(least frequently used)
volatile-ttl
volatile-random
allkeys-lru
allkeys-lfu
allkeys-random
noeviction
config配置的时候 下划线_的key需要用中横线-
xxxxxxxxxx
127.0.0.1:6379> config set maxmemory_policy volatile-lru
(error) ERR Unsupported CONFIG parameter: maxmemory_policy
127.0.0.1:6379> config set maxmemory-policy volatile-lru
OK
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:Redis6.X主从复制+读写分离介绍
背景
单机部署简单,但是可靠性低,且不能很好利用CPU多核处理能力
生产环境-必须要保证高可用-一般不可能单机部署
读写分离是可用性要求不高、性能要求较高、数据规模小的情况;
目标
简介:Redis6.X 主从复制 1主2从架构环境准备
xxxxxxxxxx
mkdir -p /data/redis/master/data
mkdir -p /data/redis/slave1/data
mkdir -p /data/redis/slave2/data
#从节点开启只读模式(默认)
replica-read-only yes
#从节点访问主节点的密码,和requirepass一样
masterauth 123456
#哪个主节点进行复制
replicaof 8.129.113.233 6379
xxxxxxxxxx
bind 0.0.0.0
port 6379
daemonize yes
requirepass "123456"
logfile "/usr/local/redis/log/redis1.log"
dbfilename "xdclass1.rdb"
dir "/usr/local/redis/data"
appendonly yes
appendfilename "appendonly1.aof"
masterauth "123456"
xxxxxxxxxx
bind 0.0.0.0
port 6380
daemonize yes
requirepass "123456"
logfile "/usr/local/redis/log/redis2.log"
dbfilename "xdclass2.rdb"
dir "/usr/local/redis/data"
appendonly yes
appendfilename "appendonly2.aof"
replicaof 8.129.113.233 6379
masterauth "123456"
xxxxxxxxxx
bind 0.0.0.0
port 6381
daemonize yes
requirepass "123456"
logfile "/usr/local/redis/log/redis3.log"
dbfilename "xdclass3.rdb"
dir "/usr/local/redis/data"
appendonly yes
appendfilename "appendonly3.aof"
replicaof 8.129.113.233 6379
masterauth "123456"
防火墙记得关闭,或者开放对应的端口
简介:Redis6.X 主从复制 1主2从架构搭建实战
xxxxxxxxxx
#启动主
./redis-server /data/redis/master/data/redis.conf
#启动从
./redis-server /data/redis/slave1/data/redis.conf
#启动从
./redis-server /data/redis/slave2/data/redis.conf
info replication 查看状态
主从复制和读写验证
防火墙和网络安全组记得开放端口
简介:Redis6.X主从复制-读写分离原理解析
主从复制分两种(主从刚连接的时候,进行全量同步;全同步结束后,进行增量同步)
全量复制
增量复制
特点
主从复制对于 主/从 redis服务器来说是非阻塞的,所以同步期间都可以正常处理外界请求
一个主redis可以含有多个从redis,每个从redis可以接收来自其他从redis服务器的连接
从节点不会让key过期,而是主节点的key过期删除后,成为del命令传输到从节点进行删除
加速复制
主从断开重连
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:Redis6.X节点高可用监控之Sentinel介绍
背景
前面搭建了主从,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,人工干预费事费力,还会造成一段时间内服务不可用
哨兵模式介绍
Sentinel三大工作任务
监控(Monitoring)
提醒(Notification)
自动故障迁移(Automatic failover)
问题
多哨兵模式下线名称介绍
主观下线(Subjectively Down, 简称 SDOWN)
客观下线(Objectively Down, 简称 ODOWN)
仲裁 qurum
简介:Redis6.X节点高可用监控之Sentinel哨兵搭建环境准备
核心流程
环境准备
xxxxxxxxxx
#不限制ip
bind 0.0.0.0
# 让sentinel服务后台运行
daemonize yes
# 配置监听的主服务器,mymaster代表服务器的名称,自定义,172.18.172.109 代表监控的主服务器,6379代表端口,
#2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 172.18.172.109 6379 2
# sentinel auth-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
sentinel auth-pass mymaster 123456
#超过5秒master还没有连接上,则认为master已经停止
sentinel down-after-milliseconds mymaster 5000
#如果该时间内没完成failover操作,则认为本次failover失败
sentinel failover-timeout mymaster 30000
xxxxxxxxxx
port 26379
bind 0.0.0.0
daemonize yes
pidfile "/var/run/redis-sentinel-1.pid"
logfile "/var/log/redis/sentinel_26379.log"
dir "/tmp"
sentinel monitor mymaster 8.129.113.233 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel auth-pass mymaster 123456
sentinel failover-timeout mymaster 30000
xxxxxxxxxx
port 26380
bind 0.0.0.0
daemonize yes
pidfile "/var/run/redis-sentinel-2.pid"
logfile "/var/log/redis/sentinel_26380.log"
dir "/tmp"
sentinel monitor mymaster 8.129.113.233 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel auth-pass mymaster 123456
sentinel failover-timeout mymaster 30000
xxxxxxxxxx
port 26381
bind 0.0.0.0
daemonize yes
pidfile "/var/run/redis-sentinel-3.pid"
logfile "/var/log/redis/sentinel_26381.log"
dir "/tmp"
sentinel monitor mymaster 8.129.113.233 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel auth-pass mymaster 123456
sentinel failover-timeout mymaster 30000
简介:Redis6.X高可用实战 主从+Sentinel哨兵集群搭建实战
xxxxxxxxxx
./redis-server /usr/local/redis/conf/sentinel-1.conf --sentinel
./redis-server /usr/local/redis/conf/sentinel-2.conf --sentinel
./redis-server /usr/local/redis/conf/sentinel-3.conf --sentinel
网络安全组需要开放端口
优点
缺点
简介: 新版SpringBoot/微服务cloud整合Redis主从+Sentinel哨兵
xxxxxxxxxx
sentinel:
master: mymaster
nodes: 8.129.113.233:26379,8.129.113.233:26380,8.129.113.233:26381
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介: Redis6.X节点高可用之Cluster集群介绍
背景
什么是集群Cluster
例子:小滴课堂-老王,去银行存钱
Anna小姐姐是窗口一的工作人员,包括取号、开户、存钱、挂失、对公业务等等
问题一
问题二
Redis集群模式介绍
简介: Redis6.X节点高可用之Cluster数据分片和虚拟哈希槽介绍
背景
常见的数据分区算法
哈希取模
范围分片
一致性Hash分区
redis cluster集群没有采用一致性哈希方案,而是采用【数据分片】中的哈希槽来进行数据存储与读取的
什么是Redis的哈希槽 slot
Redis集群预分好16384个槽,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中
大体流程
假设主节点的数量为3,将16384个槽位按照【用户自己的规则】去分配这3个节点,每个节点复制一部分槽位
存储查找
使用哈希槽的好处就在于可以方便的添加或移除节点。
简介: Redis6.X节点高可用之Cluster集群环境准备
说明
注意点:
节点(网络安全组开放端口)
xxxxxxxxxx
bind 0.0.0.0
port 6381
daemonize yes
requirepass "123456"
logfile "/usr/local/redis/log/redis1.log"
dbfilename "xdclass1.rdb"
dir "/usr/local/redis/data"
appendonly yes
appendfilename "appendonly1.aof"
masterauth "123456"
#是否开启集群
cluster-enabled yes
# 生成的node文件,记录集群节点信息,默认为nodes.conf,防止冲突,改为nodes-6381.conf
cluster-config-file nodes-6381.conf
#节点连接超时时间
cluster-node-timeout 20000
#集群节点的ip,当前节点的ip
cluster-announce-ip 172.18.172.109
#集群节点映射端口
cluster-announce-port 6381
#集群节点总线端口,节点之间互相通信,常规端口+1万
cluster-announce-bus-port 16381
xxxxxxxxxx
bind 0.0.0.0
port 6386
daemonize yes
requirepass "123456"
logfile "/usr/local/redis/log/redis6.log"
dbfilename "xdclass6.rdb"
dir "/usr/local/redis/data"
appendonly yes
appendfilename "appendonly6.aof"
masterauth "123456"
cluster-enabled yes
cluster-config-file nodes-6386.conf
cluster-node-timeout 20000
cluster-announce-ip 172.18.172.109
cluster-announce-port 6386
cluster-announce-bus-port 16386
简介: Redis6.X节点高可用之Cluster集群三主三从搭建实战
xxxxxxxxxx
./redis-server ../conf/cluster/redis1.conf
./redis-server ../conf/cluster/redis2.conf
./redis-server ../conf/cluster/redis3.conf
./redis-server ../conf/cluster/redis4.conf
./redis-server ../conf/cluster/redis5.conf
./redis-server ../conf/cluster/redis6.conf
加入集群(其中一个节点执行即可)
xxxxxxxxxx
./redis-cli -a 123456 --cluster create 172.18.172.109:6381 172.18.172.109:6382 172.18.172.109:6383 172.18.172.109:6384 172.18.172.109:6385 172.18.172.109:6386 --cluster-replicas 1
xxxxxxxxxx
./redis-cli -a 123456 --cluster check 172.18.172.109:6381
简介: Redis6.X节点高可用之Cluster集群读写命令
xxxxxxxxxx
./redis-cli -c -a 123456 -p 6379
#集群信息
cluster info
#节点信息
cluster nodes
测试集群读写命令set/get
操作都是主节点操作,从节点只是备份
流程解析
启动应用
加入集群
从节点请求复制主节点(主从复制一样)
简介: Redis6.X节点高可用之Cluster集群整合SpringBoot2.X
不在同个网络,所以集群改为阿里云公网ip地址才可以访问
xxxxxxxxxx
#对外的ip
cluster-announce-ip 8.129.113.233
#对外端口
cluster-announce-port
#集群桥接端口
cluster-announce-bus-por
动态修改配置
连接池添加 (之前添加)
xxxxxxxxxx
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
xxxxxxxxxx
cluster:
#命名的最多转发次数
max-redirects: 3
nodes: 8.129.113.233:6381,8.129.113.233:6382,8.129.113.233:6383,8.129.113.233:6384,8.129.113.233:6385,8.129.113.233:6386
简介: Redis6.X节点高可用之Cluster集群故障自动转移实战
流程
命令
xxxxxxxxxx
./redis-cli -c -a 123456 -p 6381
#集群信息
cluster info
#节点信息
cluster nodes
高可用架构总结
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介: 新版Redis6核心特性介绍-多线程
新版Redis6特性讲解
支持多线程
xxxxxxxxxx
io-threads-do-reads yes
io-threads 线程数
官方建议 ( 线程数小于机器核数 )
开启多线程后,是否会存在线程并发安全问题?
不会有安全问题,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。
简介: 新版Redis6核心特性介绍-acl 权限控制
引入了 ACL(Access Control List)
之前的redis没有用户的概念,redis6引入了acl
可以给每个用户分配不同的权限来控制权限
通过限制对命令和密钥的访问来提高安全性,以使不受信任的客户端无法访问
提高操作安全性,以防止由于软件错误或人为错误而导致进程或人员访问 Redis,从而损坏数据或配置
常用命令
xxxxxxxxxx
+<command> 将命令添加到用户可以调用的命令列表中,如+@hash
-<command> 将命令从用户可以调用的命令列表中移除
#切换默认用户
auth default 123456
#例子 密码 123 ,全部key,全部权限
acl setuser jack on >123 ~* +@all
#例子 密码 123 ,全部key,get权限
acl setuser jack on >123 ~* +get
参 数 | 说明 |
---|---|
user | 用户 |
default | 表示默认用户名,或则自己定义的用户名 |
on | 表示是否启用该用户,默认为off(禁用) |
#... | 表示用户密码,nopass表示不需要密码 |
~* | 表示可以访问的Key(正则匹配) |
+@ | 表示用户的权限,“+”表示授权权限,有权限操作或访问,“-”表示还是没有权限; @为权限分类,可以通过 ACL CAT 查询支持的分类。+@all 表示所有权限,nocommands 表示不给与任何命令的操作权限 |
简介: 新版Redis6核心特性介绍-客户端缓存
新版Redis6特性讲解
client side caching客户端缓存
类似浏览器缓存一样
详细: 分为两种模式
xxxxxxxxxx
redis在服务端记录访问的连接和相关的key, 当key有变化时通知相应的应用
应用收到请求后自行处理有变化的key, 进而实现client cache与redis的一致
这需要客户端实现,目前lettuce对其进行了支持
默认模式
Server 端全局唯一的表(Invalidation Table)记录每个Client访问的Key,当发生变更时,向client推送数据过期消息。
广播模式
愿景:"让编程不再难学,让技术与生活更加有趣"
更多架构课程请访问 xdclass.net
简介:全方位Redis6.x之战超多案例+最佳实践专题课程总结
Redis的还有很多知识+实战,基本任何一个高并发项目都离不开
综合项目大课训练营 -笔记查看(不对外提供,【原创资料】)
简介:小滴课堂架构师学习路线和大课训练营介绍
小滴课堂永久会员 & 小滴课堂年度会员
适合IT后端人员,零基础就业、在职充电、架构师学习路线-专题技术方向
学后水平:一线城市 15~25k
适合人员:校招、应届生、培训机构出、工作1~10年的同学都适合
学到技术,涨薪是关键,付出肯定会有收获,未来 一定是持续学习的人
我也在学习每年参加各种技术沙⻰和内部分享会投 入超过10万+,但是我认为是值的,且带来的收益更 大
大课综合训练营
适合用于专项拔高,适合有一定工作经验的同学,架构师方向发展的训练营,包括多个方向
综合项目大课训练营
海量数据分库分表大课训练营
架构解决方案大课训练营
全链路性能优化大课训练营
安全攻防大课训练营
数据分析大课训练营
算法刷题大课训练营
直播小班课(留意官网即可或者联系我即可)
技术人的导航站(涵盖我们主流的技术网站、社区、工具等等,即将上线)
小滴课堂,愿景:让编程不在难学,让技术与生活更加有趣
相信我们,这个是可以让你学习更加轻松的平台,里面的课程绝对会让你技术不断提升
欢迎加小D讲师的微信: xdclass-lw
我们官方网站:https://xdclass.net
千人IT技术交流QQ群: 718617859
重点来啦:加讲师微信 免费赠送你干货文档大集合,包含前端,后端,测试,大数据,运维主流技术文档(持续更新)