** 讲之前先来了解以下技术的发展归类,这对于学习java有一定的引导型**
接下来看看现在的解决方案
可以采用集群的方式,也可以采用分布式的方式。但是会发现有一个问题?
用户访问ngnix,然后nginx把服务分配给了登陆服务器它去数据库查了一些用户信息,但是当用户再次发送请求到nginx时,nginx分配给其它服务器但是用户的信息没有带过来,这就是个很大的问题?怎么解决呢。
利用session就好了,session的范围是整个浏览器的,只要会话没关就行,实现session传数据有几种方式。
方案对比
方案1:使用Tomcat内置的Session复制方案SimpleTcpCluster 优点:内置,便于服务器水平扩展,对应用无入侵
缺点:只适合Tomcat小集群,不适合大集群,因为session复制是all to all的方式,性能低,内存消耗
方案2:使用第三方(个人)基于Tomcat实现的Session管理 tomcat-session-manager 优点:已经实现对tomcat7的支持,对应用无入侵
缺点:第三方支持,支持力度不够,尤其是不能提供对Tomcat8的支持
方案3:使用Spring Session实现 基于redis存储实现 优点:不依赖于特定容器,官方支持,适合大集群,能适应各种负载均衡策略,扩展能力强
缺点:对应用有入侵,需增加相关配置。
方案4:nginx ip_hash技术
优点:配置简单,无代码修改
缺点:服务器重启丢失问题,单点负载过高,单点故障问题
其实减轻io压力还有其它的方式
把数据库服务器进行分类,有文档数据库,列式数据库,还可以把数据库进行水平切分和垂直切分。
总结:NoSQL可以减轻CPU及服务器访问压力,直接从内存中取,可以做为缓存数据库减少io的读操作
点击了解详情
几种常见的NoSQL数据库
正式进入redis
** 引言**
在Web应用发展的初期,那时关系型数据库受到了较为广泛的关注和应用,原因是因为那时候Web站点基本上访问和并发不高、交互也较少。而在后来,随着访问量的提升,使用关系型数据库的Web站点多多少少都开始在性能上出现了一些瓶颈,而瓶颈的源头一般是在磁盘的I/O上。而随着互联网技术的进一步发展,各种类型的应用层出不穷,这导致在当今云计算、大数据盛行的时代,对性能有了更多的需求,主要体现在以下四个方面:
低延迟的读写速度:应用快速地反应能极大地提升用户的满意度
支撑海量的数据和流量:对于搜索这样大型应用而言,需要利用PB级别的数据和能应对百万级的流量
大规模集群的管理:系统管理员希望分布式应用能更简单的部署和管理
庞大运营成本的考量:IT部门希望在硬件成本、软件成本和人力成本能够有大幅度地降低
为了克服这一问题,NoSQL应运而生,它同时具备了高性能、可扩展性强、高可用等优点,受到广泛开发人员和仓库管理人员的青睐。
Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库,其具备如下特性:
Redis 的应用场景包括:缓存系统(“热点”数据:高频读、低频写)、计数器、消息队列系统、排行榜、社交网络和实时系统。
点击了解详情
详解:
黄牛买票是一个单线程,但是其他人找黄牛买票就是一个多路io复用
黄牛值专心做一件事那就是买票,而其他人可以只要告诉黄牛我需要哪里的票就行了,告诉黄牛一声之后,该人不需要在那一直等待票,该人可以去干其它的事情,当黄牛买到后就会通知你,你来取票即可。这样cpu就会一直工作就实现了单线程高效率的目的。
redis命令大全
String的相关命令
演示append命令:
演示strlen:
演示setnx:
演示incr
** 如果你想根据情况去加或减可以使用下面命令**
演示:
List常用命令
简介:
命令:
演示lpush:
演示rpush:
演示lpop:
演示lindex:
演示linsert:
演示lrem:
演示lset:
集合set
简介:
set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复的数据时,set是一个很好的选择。并且set提供了判断某个成员是否在一个set集合内的重要接口,这也是List不能提供的。set是一个无序集合,底层其实就是一个value为null的hash表,所以增删,查的复杂的都是O(1)。
命令:
演示sadd,smenbers:
演示sismenber:
==哈希(hash)
简介:
映射表举例:
当要存储一个对象时:用户ID为查找的key,存储d的value用户对象包括:年龄,姓名,生日等,如果用普通的key/value结构存储主要有两种方式:
详解:
第一种缺点:当需要修给用户的属性时,要先反序列化,改好后在序列化回去,开销大
第二种缺点:用户数据冗余,存储太分散
于是就引出了第三种的方式:
解决了以上两种方式的缺点(利用映射表)
命令:
演示hkeys,hvals:
演示hincrby:
演示hsetnx:
有序集合Zset
简介
redis有序集合zset和普通集合set非常相似,是一个没有重复元素的字符串集合,不同之处是有序集合的每个成员都关联一个评分,这个评分被用来按照从最低分到最高分的方式排序集合中的成员,==集合的成员是唯一的,但是评分可以是重复的。==因为元素是有序的,所以你可以很快根据评分或次序去获得一个范围的值。访问有序集合中间的元素时也是非常快的。
** 当评分重复时它是按照顺序进行排序的**
演示zrank:
** zset底层使用了两个数据结构**
链表:
演示geoadd:
** 注意:两级无法直接添加,一般会下载城市数据,直接通过java程序一次性导入。有效的经度从-180到180,纬度从-85.05112878到+85.05112878度,当坐标位置超过指定范围时,该命令将会返回一个错误**
演示geodist:
** 默认为m,也可以自己设置,设置如下图**
它是一种消息通信模式:发送者发送消息,订阅者接收消息。redis客户端可以订阅任意数量的频道。
演示发布和订阅:
打开两个客户端:一个作订阅者,一个作发布者
redis.clients
jedis
3.3.0
package org.example;
import redis.clients.jedis.Jedis;
import java.util.Set;
public class TestJedis {
public static void main(String[] args) {
//创建redis连接
Jedis jedis = new Jedis("192.168.98.134", 6379);
//jedis.auth("123");//输入密码,没有设置则不需要写
String set = jedis.set("username", "john");
System.out.println(set);//加入数据后的返回结果
String username = jedis.get("username");
System.out.println(username);
String mset = jedis.mset("k2", "v2", "k3", "v3");//设置多个值
Set keys = jedis.keys("*");
for (String key : keys) {
System.out.println(key);
}
System.out.println(jedis.exists("k1"));
jedis.hset("user:1001","name","zhangsan");//hash类型
System.out.println(jedis.hget("user:1001", "name"));
jedis.close();//关闭连接避免浪费资源
}
}
org.springframework.boot
spring-boot-starter-data-redis
2.4.4
org.apache.commons
commons-pool2
2.6.0
#redis服务器地址
spring.redis.host=192.168.98.134
#redis服务端口
spring.redis.port=6379
#redis数据库索引
spring.redis.database=0
#连接超时时间
spring.redis.timeout=1800000
#连接池最大连接数(负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池最小空闲连接
spring.redis.lettuce.pool.min-idle=0
package com.chao.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
public class RedisConfig {
@Bean
public RedisTemplateredisTemplate(RedisConnectionFactory factory){
RedisTemplatetemplate=new RedisTemplate<>();
//关联
template.setConnectionFactory(factory);
//设置key的序列化器
template.setKeySerializer(new StringRedisSerializer());
//设置value的序列化器
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return template;
}
}
** 为什么要序列化呢?因为对象是以json进行存取的,需要序列化和反序列化**
package com.chao.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/redisTest")
public class TestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
redisTemplate.opsForValue().set("name","xiaoming");
String value = (String) redisTemplate.opsForValue().get("name");
return value;
}
}
详解:redis底层就是一条条的指令,这些指令就是一个原子性的操作,cpu对指令只有两种情况,1. 执行失败。2. 执行成功。中断是发生在指令之间的操作。在java中很常见,因为一条Java语句被cpu执行起来可能是多条指令,而cpu只认指令不人其它的。所以这样的就不是原子操作。原子性是最小的执行单元。
简介:
redis事务是一个单独的隔离操作,事务中的所有的命令都会序列化,按顺序执行。事务在执行过程中,不会被其它客户端发送的命令请求打断。redis事务主要作用就是串联多个命令防止别的命令插队。
命令:
以上演示是执行成功的事务,当事务出现错误会出现什么呢?
事务的错误的两种情况
第二个客户端:
第一个加10 :
第二个加20:
为什么会出现回滚呢?
因为当开启该key的监听时,针对该值会有一个版本号,假设为v1.0,两个客户端分别开启事务并组队,这时原版本号v1.0会同步到两个客户端中,当第一个客户端执行的时候会先检查我拿的版本号是否和原版本号相同,一比较发现相同,则exec(执行)成功,结果加10原值为110,此时会生成一个新的版本号,假设为v1.1,第二个客户端在执行的时候也会先检查我拿的版本号是否和现在的版本号相同,一比较发现不同,证明该值已经别其它事务修改过,所以该事务执行失败(回滚)。当事务执行完后(不管是否成功其监听模式都会被取消)
持久化有两种方式RDB,AOF
什么时RDB
在指定时间间隔内将内存中的数据快照写入磁盘,恢复时将快照文件直接读到内存里。
RDB是如何执行过程或原理
为什么不直接同步到.rdb文件中,而是采用临时文件替换上次持久化好的文件?
如果直接就同步到.rdb文件中会导致数据的不完整性,数据的不一致性,如果现在有10个数据但是当同步到第8个数据时服务器挂掉了,就造成了数据的不完整性
RDB的优缺点
先把它改为yes:
aof的保存路径是和rdb一样的,rdb变它就变
修改配置文件后要重新启动redis服务会发现bin目录下多了一个文件
AOF和RDB同时启动后,redis是怎么加载的呢?
因为当两个同时存在时,redis优先加载AOF的数据,但此时该文件数据为空,所以redis里啥都没有。
为什么优先读AOF呢?因为AOF相比于RDB数据更完整,一致性更高。
记住:redis要是没有进行持久化,只要该台redis挂了那么在重启之前的数据是不会有的,就相当于一个全新的redis服务器,只有加了持久化redis在重启时会先读取持久化文件,该文件里的数据会被恢复到redis服务器里。
AOF可以进行异常恢复
如果遇到AOF文件损坏,则可以用/usr/local/bin/redis-check-aof–fix appendonly.aof进行恢复。
现在制造个异常:在该文件中添加一个hello,很显然这个数据格格不入,因为该文件保存数据的格式不是这样的。所以这就是一个异常。
重启服务,重连客户端:
连接不上,这是因为在启动redis时会读取 appendonly.aof配置文件,这时肯定会出错
重启服务重新连接客户端:成功
数据的备份和rdb相同这里就不再赘述
Rewrite 压缩
是什么?
简单来说就是当该文件的数据存不下了,达到阈值了,就会触发重写机制,它会把之前多个指令合并为一个指令,不注重过程了只注重结果了,只要能恢复数据就行,其它的都可以不要以达到压缩的效果。
重写的原理相当于rdb的写时复制技术
重写虽然可以节省磁盘空间,减少恢复时间,但是每次重写还是有一定的负担,因此一定要设置重写的条件:
AOF持久化的流程
最后一步重启redis服务,会加载AOF文件中的写操作达到恢复数据的目的。
AOF的优点:
概念:
主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,master以写为主,slaver以读为主。
能干什么呢?
如果主服务器挂了该怎么办呢?主服务器就一台啊。能不能搞个多主多从呢?肯定是不行的,为什么呢?
因为:假如有两台主服务器同时都set相同的key但是value不同,这时从服务器就不知道该复制谁的数据了。
该怎么解决呢?
用集群即可。复制多个一主多从,主服务器要是挂了,可以用其它的主服务器来保证正常运行。
这里是用单机操作,在实际场景中是在不同的电脑上搭建的。
有两种方法:第一复制文件该端口号就行,第二是新建配置文件,把之前配置文件中数据包含进去,然后写一些配置就行,因为启动多个服务就会有多个配置文件要设置,但是多个配置文件中有一些数据时公共的,所以可以用第二种更为简单。本文配置采用第二种:
此时这三个还都是独立的服务器,可以用info replication查看运行情况
7. 在从机上通过slaveof + 主机ip + 端口号:
到6379看一下信息:
依照上面的步骤把6381也加进去:
最后结果如下:
至此一主两从就搭建好了
接下来演示一下读写效果
在主机中写入:
在两个从机中能读到:
如果在从机中进行写操作则会报:
当从服务器挂掉后,重新启动该服务器会发生什么呢?
假设6380从机挂了
6379主机会只有一台从服务器了:
当重启后该从机会变成主机:
现在重新把它设置为从服务器:
当重新成为从机时它会重新从主机中复制数据(这是从机挂在从机的一个特点)
当主服务器挂掉会发生什么呢?
从机会:
重新启动6379服务:
6379还是主机吗?它的两个从机还在吗?
说明一下:192.168.98.135和127.0.0.1是一样的,后者映射到前者
主从复制原理:
讲解:第一次进行数据同步是从服务器主动向主服务器发送请求信息的,什么是第一次呢?就是在从服务器设置了slaveof + ip地址+ 端口号时。
之后呢,每次主机进行写操作时,数据的同步是主机主动发给从机的,而从机主动发送请求只发生在第一次。
概念:
从服务器只对应一个从机,但是从机又跟一个从机,以此类推
薪火相传策略的问题的答案是和一主多从一样:从服务器挂了或者主服务器挂了会怎么样。
概念:
当主服务器挂了,让从服务器变成主服务器的这一过程被称为反客为主
演示一下:
让主机挂掉:
让6380变成主机:
执行该命令会:redis从属服务器执行命令 SLAVEOF NO ONE 将使得这个从属服务器关闭复制功能,并从从属服务器转变回主服务器,原来同步所得的数据集不会被丢弃。
当原来的主机重启后那么它不再是主机了,而是6380的从机。
但是这中模式有一个问题:在实际的业务场景中,主机挂了后,可能不能第一时间手动让从机变为主机,于是就引入了哨兵模式:让从机自动的变为主机。
讲解:主机挂了,哨兵立刻检测到并同时让一台从机切换为主机,当主机好了时,它却变成了从机。
演示:
先把模式调为一主而从的模式
创建并编辑sentinel.conf
里面只写一句话:
选择条件依次是:
讲解:优先级就不用说了,上面写的很清楚了。当优先级一样就看偏移量:就是从机中谁里面的数据和主机的数据差距最小,就选谁,比如:主机中有10个数据,从机1同步到了9个,从机2同步到了8个,这时就选从机1作为主机,当偏移量一样就看runid(选择最小的)。
单机结构我想大家最最最熟悉的就是单机结构,一个系统业务量很小的时候所有的代码都放在一个项目中就好了,然后这个项目部署在一台服务器上就好了。整个项目所有的服务都由这台服务器提供。这就是单机结构。
那么,单机结构有啥缺点呢?我想缺点是显而易见的,单机的处理能力毕竟是有限的,当你的业务增长到一定程度的时候,单机的硬件资源将无法满足你的业务需求。此时便出现了集群模式,往下接着看。
集群结构集群模式在程序猿界有各种装逼解释,有的让你根本无法理解,其实就是一个很简单的玩意儿,且听我一一道来。单机处理到达瓶颈的时候,你就把单机复制几份,这样就构成了一个“集群”。集群中每台服务器就叫做这个集群的一个“节点”,所有节点构成了一个集群。每个节点都提供相同的服务,那么这样系统的处理能力就相当于提升了好几倍(有几个节点就相当于提升了这么多倍)。
但问题是用户的请求究竟由哪个节点来处理呢?最好能够让此时此刻负载较小的节点来处理,这样使得每个节点的压力都比较平均。要实现这个功能,就需要在所有节点之前增加一个“调度者”的角色,用户的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这个“调度者”有个牛逼了名字——负载均衡服务器。
集群结构的好处就是系统扩展非常容易。如果随着你们系统业务的发展,当前的系统又支撑不住了,那么给这个集群再增加节点就行了。但是,当你的业务发展到一定程度的时候,你会发现一个问题——无论怎么增加节点,貌似整个集群性能的提升效果并不明显了。这时候,你就需要使用微服务结构了。
分布式结构先来对前面的知识点做个总结。从单机结构到集群结构,你的代码基本无需要作任何修改,你要做的仅仅是多部署几台服务器,每台服务器上运行相同的代码就行了。但是,当你要从集群结构演进到微服务结构的时候,之前的那套代码就需要发生较大的改动了。所以对于新系统我们建议,系统设计之初就采用微服务架构,这样后期运维的成本更低。但如果一套老系统需要升级成微服务结构的话,那就得对代码大动干戈了。所以,对于老系统而言,究竟是继续保持集群模式,还是升级成微服务架构,这需要你们的架构师深思熟虑、权衡投入产出比。OK,下面开始介绍所谓的分布式结构。
分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。
举个例子,假设需要开发一个在线商城。按照微服务的思想,我们需要按照功能模块拆分成多个独立的服务,如:用户服务、产品服务、订单服务、后台管理服务、数据分析服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。
这样的好处有很多:系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。
系统之间的耦合度降低,从而系统更易于扩展。我们可以针对性地扩展某些服务。假设这个商城要搞一次大促,下单量可能会大大提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,而对于后台管理系统、数据分析系统而言,节点数量维持原有水平即可。
服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。
对相应的配置文件进行跟6379相同的配置:这里就举一个例子,剩余的自己解决:修改6381的配置文件:
3. 启动6个服务
首先你要检查一下你的redis有没有ruby环境
到你自己安装的redis目录下:我的在这个目录下:
版本低的redis没有集成这个环境,但是高的redis已经集成了
命令合成集群:
演示:必须在src目录下执行该命令:因为该命令是依赖于rb环境的:
从上面可以看到6390为6379的从机,6391为6380的从机,6389为6381的从机
点击yes:
注意:不能用普通方式连接客户端,否则就不是集群了
用该命令连接才是集群的方式:才能体现出redis的无中心化(各个服务都可以作为入口,它可以自动切换你要访问的那个服务)
演示:
查看节点信息(查看集群状态)
插槽就是为了保证数据被平均分配到不同的服务器中
redis集群共有16384个插槽,然后平均分配到三个主机上,结果如下:
注意:当你同时插入多条值时会:
那么该如何添加多个值呢?可以利用组的概念。
演示:cluster keyslot +key值
演示:cluster getkeysinslot + 插槽值 +个数
当主机挂了后,从机立刻上位成主机,当原来的主机恢复后只能当从机了。
当主机和从机都挂了呢?整个服务就会瘫痪吗?不是的,要根据你的配置决定。
用Jedis主要是通过java去控制redis服务,只需要用Java连进去就行,所以连接集群的方式为HostAndPort hostAndPort = new HostAndPort("192.168.98.135",6379); JedisCluster jedisCluster = new JedisCluster(hostAndPort);
点击了解详情
为什么使用分布式锁,(分布式锁的概念)
用setnx命令去操作数据(setnx只能加数据库里不存在的key值,若存在则不会加成功)
解释:也就是说setnx相当于有锁的机制,当你set一个k1时,会对这个k1上一把锁,其它服务则不能去操作它了,知道这把锁被释放,那么怎么被释放呢?其实把它删除就行了。
但是这样就万事大吉了吗。当然不是,它会有问题的:要是该锁因外部原因(服务挂了等原因)导致锁一直内有释放,那么其它服务就不能用适用该数据了,解决它很简单:加一个过期时间就好了
这样虽然解决了上述问题,但是又引发了另一个问题,如果由于某种原因导致没有执行到设置过期时间,那么问题就还是之前那个问题了,所以可以采用边塞值边设置过期时间,也就是说让它俩同时进行
演示:
这样虽然可以解决上述问题,但是它真的适用于任何场景吗?假如有一个场景如下:
场景详解:对于同一个数据,假如a先抢到该数据,那么它会被上锁,然后执行具体操作,但是在执行过程中服务器突然卡顿,卡顿的时间超过了过期时间10s,这时锁会被自动释放。锁一被释放,假如服务b抢到了这把锁,然后它也去做具体操作,但在执行过程中,服务a又反应过来了,执行之前未完成的操作,然后手动释放锁,那么问题来了:它这时释放的是服务b的锁。这肯定是不对的,锁只能释放自己的,不可以去释放被人的,否则不就乱套了吗。于是就引入了(UUID防止误删)。
解决方案
虽然这样可以解决上述问题,但是你想想服务卡顿是在具体操作的过程中发生的,如果它发生在删除操纵过程中呢?还是会出现删除别人的锁。这就是原子性造成的问题。
删除锁的原子性问题
场景如下图:
解决方案
使用lua脚本:
也就是说一旦上锁了直到释放锁后,别人才能拿到这把锁,不管在上锁和释放锁的过程中发生了什么,别人是不肯拿到这把锁的
至此redis分布式锁就讲完了
常用命令:
详解:user1代表用户名,on 代表开启,123456代表密码,(~cached:)代表该用户只能对cached:的key进行操作,+get代表该用户只能执行get命令。
切换用户:
redis至此就结束了,后续如有研究会继续更新