1 Redis
Redis下载:苍老师网站
1.1 什么是Redis?
Redis就是一个能够将信息或数据保存在内存中的缓存数据库。
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。目前Redis的开发由Redis Labs赞助。根据月度排行网站DB-Engines.com的数据,Redis是最流行的键值对存储数据库。
Redis是一个开发好的软件,有固定的使用方式。
Redis的特征:
-
Redis是一个内存(缓存)数据库,因为数据保存在内存中,所以速度快,每秒可执行10万次读写操作。
-
虽然Redis是一个内存数据库,但是它允许将数据保存在硬盘上,以便出现运行异常时恢复(Redis数据保存到硬盘上的策略有两种:AOF和RDB,可同时开启)
-
Redis保存数据使用key-value的格式,类似java中的Map类型集合,这样使用key-value保存数据的数据库统称为"非关系型数据库",英文"no-sql"(关系型数据库,sql,通过外键等建立关系)
-
Redis的key为string类型,value支持各种类型:string、list、set、zset、hash。
-
Redis支持微服务系统需要的分布式部署,支持master-slave(一主多从)的模式,以达到"高并发、高可用、高性能"的目的
-
Redis的竞品软件memcached,关于它们的区别可以自学
-
Redis是一个缓存数据库,以键值对的形式将数据保存到内存中,属于非关系型数据库(nosql),特点是快速响应
-
内存数据断电消失
-
Redis将数据保存到硬盘的两种方式:AOF、RDB,可同时进行
-
有些数据库软件也以key-value形式将数据保存到硬盘上,但是是关系型数据库(sql),如:MongoDB
-
关系型数据库主要通过外键等建立关系,一般是存在硬盘上,主要用途是用来保存全部数据,如:MySQL、Oracle、MongoDB、ServerSocket
-
非关系型数据库主要以键值对形式将数据保存到内存中,主要是辅助关系型数据库使用,特点是响应速度快
-
注意:并非所有的非关系型数据库数据都存在内存中,也有存在硬盘中的
-
Redis的竞品memcached,就性能而言,memcached性能好,但是redis可以进行备份到硬盘
-
redis可用于数据进行增减的场合,如商品抢购
-
redis数据存在内存中,只有重启电脑数据才会清除,一般是1小时无人访问就会自动回收,但是这不是绝对的,一般redis会根据内存分配灵活配置
-
1.2 为什么需要Redis?
如下图所示,当faq模块由多台服务器组成时,每个服务器都要缓存一份所有标签,这样会造成缓存冗余,造成内存的浪费。我们可以使用Redis保存所有标签,当任何faq服务器需要时直接从Redis中获取即可,这样节省了内存,提高了服务器性能。
1.3 Redis的安装及初步使用
解压运行下载的Redis,文件夹内容如下所示:
-
双击redis-start.bat文件可以启动redis,出现一个界面,这个界面不能关,一关redis就停止了
-
redis-cli.exe可以运行操作Redis的客户端
由于每次开机都要启动redis,界面还不能关,不方便。
我们可以实现每次开机自动启动,需要运行下面的文件:
-
先运行: service-installing.bat---安装Redis服务到操作系统
-
再运行:service-start.bat---启动Redis服务
-
如果想停止服务: service-stop.bat
-
如果想卸载: service-uninstalling.bat
运行上面的步骤1和2即可,正常情况下,每次开机redis都会自动开启了,然后打开redis-cli.exe,启动后输入info,输出下面的信息就表示redis启动成功了,之后可以在此界面进行代码的编写:
1.4 Redis基本操作
Redis支持如下几种数据类型:
我们主要使用string字符串类型,string类型基本操作如下:
127.0.0.1:6379> set mystr "hello world!" //保存字符串类型
127.0.0.1:6379> get mystr //读取字符串类型
除了保存字符串,Redis还特别适合保存频繁变化的数字。因为如果频繁修改硬盘数据库(mysql)中的数字的话,每次都是硬盘操作,效率低;如果修改的是redis中的数据,那么支持的并发会较大。
除此之外,Redis是一个单线程的程序,没有线程安全问题,所有即使是高并发的程序也能够正确响应数字的变化。
下面是对数字增减的专门命令:
127.0.0.1:6379> set mynum "2" //创建数字,也可直接写数字保存:set mynum 2,默认保存数字为字符串类型
OK
127.0.0.1:6379> get mynum //查询数字
"2"
127.0.0.1:6379> incr mynum //数字增加
(integer) 3
127.0.0.1:6379> get mynum //查询数字
"3"
127.0.0.1:6379> decr mynum //数字减少
(integer) 2
127.0.0.1:6379> get mynum //查询数字
"2"
Redis基本命令补充:
List 列表
常用命令: lpush,rpush,lpop,rpop,lrange等
Redis的list在底层实现上并不是数组而是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
lists的常用操作包括LPUSH、RPUSH、LRANGE、RPOP等。可以用LPUSH在lists的左侧插入一个新元素,用RPUSH在lists的右侧插入一个新元素,用LRANGE命令从lists中指定一个范围来提取元素,RPOP从右侧弹出数据。来看几个例子::
//新建一个list叫做mylist,并在列表头部插入元素"Tom"
127.0.0.1:6379> lpush mylist "Tom"
//返回当前mylist中的元素个数
(integer) 1
//在mylist右侧插入元素"Jerry"
127.0.0.1:6379> rpush mylist "Jerry"
(integer) 2
//在mylist左侧插入元素"Andy"
127.0.0.1:6379> lpush mylist "Andy"
(integer) 3
//列出mylist中从编号0到编号1的元素
127.0.0.1:6379> lrange mylist 0 1
1) "Andy"
2) "Tom"
//列出mylist中从编号0到倒数第一个元素
127.0.0.1:6379> lrange mylist 0 -1
1) "Andy"
2) "Tom"
3) "Jerry"
//从右侧取出最后一个数据
127.0.0.1:6379> rpop mylist
"Jerry"
//再次列出mylist中从编号0到倒数第一个元素
127.0.0.1:6379> lrange mylist 0 -1
1) "Andy"
2) "Tom"
Set 集合
常用命令: sadd,smembers,sunion 等
set 是无序不重复集合,list是有序可以重复集合,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要功能,这个也是list所不能提供的。
可以基于 set 轻易实现交集、并集、差集的操作。比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能,也就是求交集的过程。set具体命令如下:
//向集合myset中加入一个新元素"Tom"
127.0.0.1:6379> sadd myset "Tom"
(integer) 1
127.0.0.1:6379> sadd myset "Jerry"
(integer) 1
//列出集合myset中的所有元素
127.0.0.1:6379> smembers myset
1) "Jerry"
2) "Tom"
//判断元素Tom是否在集合myset中,返回1表示存在
127.0.0.1:6379> sismember myset "Tom"
(integer) 1
//判断元素3是否在集合myset中,返回0表示不存在
127.0.0.1:6379> sismember myset "Andy"
(integer) 0
//新建一个新的集合yourset
127.0.0.1:6379> sadd yourset "Tom"
(integer) 1
127.0.0.1:6379> sadd yourset "John"
(integer) 1
127.0.0.1:6379> smembers yourset
1) "Tom"
2) "John"
//对两个集合求并集
127.0.0.1:6379> sunion myset yourset
1) "Tom"
2) "Jerry"
3) "John"
Sorted Set 有序集合
常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
很多时候,我们都将redis中的有序集合叫做zsets,这是因为在redis中,有序集合相关的操作指令都是以z开头的,比如zrange、zadd、zrevrange、zrangebyscore等等
来看几个生动的例子:
//新增一个有序集合hostset,加入一个元素baidu.com,给它赋予score:1
127.0.0.1:6379> zadd hostset 1 baidu.com
(integer) 1
//向hostset中新增一个元素bing.com,赋予它的score是30
127.0.0.1:6379> zadd hostset 3 bing.com
(integer) 1
//向hostset中新增一个元素google.com,赋予它的score是22
127.0.0.1:6379> zadd hostset 22 google.com
(integer) 1
//列出hostset的所有元素,同时列出其score,可以看出myzset已经是有序的了。
127.0.0.1:6379> zrange hostset 0 -1 with scores
1) "baidu.com"
2) "1"
3) "google.com"
4) "22"
5) "bing.com"
6) "30"
//只列出hostset的元素
127.0.0.1:6379> zrange hostset 0 -1
1) "baidu.com"
2) "google.com"
3) "bing.com"
Hash
常用命令: hget,hset,hgetall 等。
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:
//建立哈希,并赋值
127.0.0.1:6379> HMSET user:001 username antirez password P1pp0 age 34
OK
//列出哈希的内容
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "P1pp0"
5) "age"
6) "34"
//更改哈希中的某一个值
127.0.0.1:6379> HSET user:001 password 12345
(integer) 0
//再次列出哈希的内容
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "12345"
5) "age"
6) "34"
1.5 SpringBoot操作Redis
添加依赖:像mysql一样,java可以操作mysql数据库,就可以操作Redis。底层我们使用jdbc操作mysql,redis方面底层使用Jedis操作Redis,但是和jdbc操作数据库一样,使用Jedis操作Redis步骤比较繁琐。
我们可以使用Spring Boot Redis操作Redis,这样会很简单,先添加必要依赖,然后才能使用Spring Boot Redis框架,在knows-faq模块的pom.xml文件添加如下:
<!--Spring连接Redis的依赖,上面为底层,下面为封装优化-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
application.properties文件中需要配置Redis的ip地址和端口号,就像我们连接数据库也要提供这些资料一样。
# 配置Redis的ip和端口,localhost即127.0.0.1
spring.redis.host=localhost
spring.redis.port=6379
1.6 基本操作
我们可以在测试类中编写代码测试是否可以成功操作redis:
//我们添加Spring Redis的依赖就是向Spring容器中添加了一个可以操作Redis的对象
// RedisTemplate<[key的类型],[value的类型]>
@Autowired
RedisTemplate<String,String> redisTemplate;
@Test
public void redis(){
// 向Redis中保存(添加)数据
redisTemplate.opsForValue().set("myname","东方不败");
System.out.println("ok");
}
@Test
public void getValue(){
//读取Redis中的信息
String name=redisTemplate.opsForValue().get("myname");
System.out.println(name);
}
输出结果:
ok
东方不败
1.7 优化标签缓存
我们学习了怎么操作Redis,下面我们就将TagServiceImpl实现类中获得所有标签的方法修改为从Redis中获取。
转到knows-faq模块: TagServiceImpl 实现类代码修改如下:
@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {
//RedisTemplate源代码注入只注入了两个类型
//RedisTemplate<String, String>
//RedisTemplate<Object, Object>
//如果使用@Autowired自动装配,因为泛型类型不匹配,所以会报错
//但是@Resource是id,注入主要参数名称是redisTemplate就可以注入,成功
@Resource
private RedisTemplate<String,List<Tag>> redisTemplate;
//从Spring容器中取TagMapper
@Autowired
private TagMapper tagMapper;
//全查所有Tags
@Override //重写方法
public List<Tag> getTags() {
//先从Redis中获得所有标签的集合
List<Tag> tags = redisTemplate.opsForValue().get("tags");
//如果上面的获取失败,说明Redis中没有所要标签
//那么就是第一次请求,需要连接数据库新增
if(tags==null || tags.isEmpty()){
//连接数据库查询所有标签
tags = tagMapper.selectList(null);
//将全查出来的标签保存到redis中
redisTemplate.opsForValue().set("tags",tags);
System.out.println("Redis已加载所有标签");
}
//千万别忘了修改返回值!!!!
return tags;
}
//全查所有Tags放在Map中
@Override
public Map<String, Tag> getTagMap() {
Map<String,Tag> tagMap = new HashMap<>();
for(Tag t:getTags()){
tagMap.put(t.getName(),t);
}
//千万别忘了修改返回值!!!!
return tagMap;
}
}
重新启动knows-faq服务,访问学生首页,路径为:http://localhost:8080/index_student.html,输出效果如下:
控制台输出内容如下:
之后刷新或重启服务,不再输出该语句,因为数据已经存到redis缓冲中去了,除非重启电脑,重启后加载一次即可,以后不再需要重复加载,直接使用即可。
2 使用Ribbon实现服务间调用
注册和查询所有标签我们已经完成了,下面要完成登录功能,登录功能涉及很多知识点和代码,先来学习Ribbon。
2.1 什么Ribbon?
Ribbon是SpringCloud提供的一个组件,它能够实现微服务之间的互相调用。它的使用不用添加额外依赖,因为使用的非常频繁,在spring-cloud-starter-alibaba-nacos-discovery这个依赖中已经集成了。
2.2 Ribbon基本使用
步骤1 : 明确服务的提供者(方法的定义)
服务的提供者也叫生产者,本次调用我们将sys模块作为生成者,需要定义一个方法作为被调用的方法,必须是一个控制器方法才能被Ribbon调用,我们将/v1/auth/demo这个路径的方法作为调用目标。
步骤2 : 在发起调用的一方添加Ribbon的支持
本次调用的发起者是faq模块,在SpringBoot启动类中注入一个能够发起Ribbon请求的对象
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("cn.tedu.knows.faq.mapper")
public class KnowsFaqApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsFaqApplication.class, args);
}
// @Bean表示将下面方法的返回值保存到Spring容器中
@Bean
// 保存到Spring容器的对象支持负载均衡的Ribbon调用
@LoadBalanced
// 这个方法的返回值是实现Ribbon调用的对象,使用它来发起跨服务器的调用请求
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
步骤3 : 发起调用
使用刚刚保存到Spring容器中的RestTemplate对象调用方法,我们先使用测试类来调用。实际开发中经常会在业务逻辑层中发起,测试类代码如下:
@Autowired
RestTemplate restTemplate;
@Test
public void ribbon(){
// 声明要调用的控制器的路径:
// sys-service:要调用的微服务注册到Nacos的名称
// /v1/auth/demo:要调用的控制器的访问路径
String url="http://sys-service/v1/auth/demo";
// ribbon调用
// 参数url:是上面定义的字符串
// 参数String.class:定义返回值类型的反射,根据实际情况编写即可
String str=restTemplate.getForObject(url,String.class);
System.out.println(str);
}
测试结果:
sys:Hello World!!!
关系示意图如下:
2.3 使用Ribbon通过用户名获得对象
我们下面将刚编写的Ribbon升级,添加了参数,返回值变为了User。faq模块还是请求的发起者(消费者),根据Ribbon的规则,我们首先要在sys模块中定义一个根据用户名返回用户对象的控制器方法,没有这个方法就要编写这个方法,从业务逻辑层开始。
转到knows-sys模块:
(1)IUserService接口中添加方法:
// 根据用户名查询用户对象
User getUserByUsername(String username);
UserServiceImpl类中实现业务逻辑层方法:
//根据用户名查找用户对象的逻辑层实现
@Override
public User getUserByUsername(String username) {
return userMapper.findUserByUsername(username);
}
(2)控制层代码:AuthController添加方法
@Resource
private IUserService userService;
@GetMapping("/user")//测试路径:http://localhost:8002/v1/auth/user
public User getUser(String username){
return userService.getUserByUsername(username);
}
在faq模块中进行测试,测试执行前保证sys模块重新启动过!
@Test
public void getUser(){
// 有参数的Ribbon调用
// url请求的路径写完之后,使用?分割开始编写参数列表
// 参数的值不能写死要用{1},{2}....这种方式占位
String url="http://sys-service/v1/auth/user?username={1}";
// 调用带参数的方法
// 从第三个参数开始,给{1}赋值,第四个参数给{2}赋值,以此类推
User user=restTemplate.getForObject(url, User.class,"st2");
System.out.println(user);
}
测试结果:
需要大家下载一个软件:postman(邮递员)
下载地址:https://www.postman.com/downloads/
这个软件用于向服务器发送各种请求,get\post均可,还可以携带参数。
3 微服务的会话保持
3.1 什么是会话保持
会话就是多个请求和响应的集合,一般来讲,打开浏览器到关闭浏览器就是一次会话。
会话对应java中的HttpSession对象,所谓会话保持,就是多次请求过程中,服务器都可以获得session中的信息。
单体项目中会话保持是依靠session对象的。
3.2 微服务项目的会话保持问题
因为微服务具有多个项目,每个项目都有自己的session,在一个服务器中登录并不能共享给其它项目,这样单体项目中的会话保持方式就不能使用了。
如下图所示,sys模块登录成功并不能把登录信息发送给faq模块,这样就无法实现会话保持,微服务项目中有专门的会话保持技术称之为"单点登录"。