微服务(四)-Redis、Ribbon、会话保持、单点登录(session共享、Token令牌)

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,界面还不能关,不方便。

我们可以实现每次开机自动启动,需要运行下面的文件:

  1. 先运行: service-installing.bat---安装Redis服务到操作系统

  2. 再运行:service-start.bat---启动Redis服务

  3. 如果想停止服务: service-stop.bat

  4. 如果想卸载: 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模块,这样就无法实现会话保持,微服务项目中有专门的会话保持技术称之为"单点登录"。

 

3.3 单点登录实现思路

  上次课我们讲到了微服务项目因为有多个服务器组成,遇到了会话保持问题。

  在一个服务器上登录,能够让所有服务器知道当前用户的信息的解决方案就是单点登录

单点登录的办法有很多,但是实现思路主要有两种:

  1. Session共享

  1. Token令牌

3.3.1 方法一:Session共享

原理:让用户的登录信息共享给所有模块,用户登录时共享session。

下图表示使用Session共享实现单点登录:

基本思路:将用户信息保存到Redis中,哪个模块需要用户信息从redis中取。

优点:

  1. 安全性高

  1. 框架支持比较完善,开发代码量小

缺点:

  1. 每个模块还有Redis都要消耗较多内存保存用户信息

  1. 当用户信息更新时,需要比较复杂的操作

3.3.2 方法二:Token令牌

原理:当用户登录时,将一个加密的令牌发送给客户端,客户端保存这个令牌,令牌中包含用户信息,访问其它模块时,使用令牌表名身份。

下图是Token令牌的解决方案:

基本思路:用户登录成功颁发令牌,由客户端保存,访问其它服务器时,客户端提供令牌,服务器验证。

优点:

  1. 解放session,服务器不需要再因为用户信息占用内存

  1. 客户端保存令牌,方便响应客户端变化

缺点:

  1. 解析和颁发令牌需要Cpu消耗算力

  1. 绝对安全的业务,需要再次验证

在达内知道项目中,我们使用第二种方式实现微服务的单点登录,Token令牌这种方式有很多需要我们解决的问题,我们通过一些业界成熟的规范和标准实现这个过程。

 

posted @ 2021-09-09 23:10  Coder_Cui  阅读(641)  评论(0编辑  收藏  举报