上次课我们使用了Redis来保存一个字符串
但是实际开发中,基本上保存的都是集合或对象
我们可以保存json格式的字符串来实现
在实际开发中,我们可能会将标签分类秒杀这样或类似的数据保存在Redis中,以应对频繁的访问
实际上,还有一种比较多的使用的缓存就是某一条信息的浏览量评论数点赞数等,以及秒杀商品时的库存数等,都可以利用Redis提高并发量,高效访问
我们就来使用一下加减数字的命令
127.0.0.1:6379> set num "2" OK 127.0.0.1:6379> get num "2" 127.0.0.1:6379> incr num (integer) 3 127.0.0.1:6379> get num "3" 127.0.0.1:6379> decr num (integer) 2
笔记末尾有Redis操作其它类型的演示操作,同学们可以自己运行测试
Redis线程安全问题
Redis底层操作数据的线程只有一条,即使有并发请求,也不会有线程安全问题,因为足够快,所以一条线程也能快速处理数据,请求不会有明显的等待
SpringBoot操作Redis 添加依赖转到knows-faq模块
在pom.xml文件中添加如下依赖
redis.clients jedis org.springframework.data spring-data-redis
Redis也是数据库,java操作Redis和java操作mysql有很多相似
java是通过jdbc来操作mysql数据库的
java是通过jedis来操作redis数据库的
jdbc和jedis都是比较繁琐的操作数据库的代码
所以我们使用mybatis操作mysql更简单
我们使用Spring-data-redis也可以更简单的操作redis
在操作之前我们还要配置一下application.properties文件,指定Redis的位置
knows-faq模块的application.properties文件
# 配置Redis的ip地址和端口号,以便SpringDataRedis连接 spring.redis.host=localhost spring.redis.port=6379基本操作测试
我们配置完SpringDataRedis
最好先在测试类中进行一下测试,保证运行状况良好
测试代码如下
// 下面代码时要连接Redis来进行新增和获取的操作 // 注解标记的对象可以操作Redis // 是SpringDataRedis框架自动向Spring容器中保存的对象 // RedisTemplate<[key类型],[value的类型]> @Resource RedisTemplateRedis缓存标签列表redisTemplate; // 新增数据 @Test public void add(){ redisTemplate.opsForValue().set("myname","诸葛亮"); System.out.println("ok"); } // 获取数据 @Test public void get(){ String name=redisTemplate.opsForValue().get("myname"); System.out.println(name); }
继续在knows-faq模块
我们的目标是TagServiceImpl类中查询出所有标签保存在Redis中
而现在我们在使用List
TagServiceImpl代码修改后结果如下
@Service public class TagServiceImpl extends ServiceImplimplements ITagService { @Autowired private TagMapper tagMapper; // 获得操作Redis的对象 @Resource private RedisTemplate > redisTemplate; @Override public List getTags() { // 先编写代码,从Redis中获得所有标签 List tags=redisTemplate.opsForValue().get("tags"); // 判断从Redis中获得的标签是否为空 if(tags==null){ // 如果为空,证明是第一次访问,需要连接数据库查询所有标签 tags=tagMapper.selectList(null); // 将查询出的所有标签保存在Redis中,以便下次获取 redisTemplate.opsForValue().set("tags",tags); System.out.println("Redis加载所有标签完毕"); } // 返回所有标签 return tags; } @Override public Map getTagMap() { // 实例化一个map对象 Map tagMap=new HashMap<>(); // 遍历上面获得List的方法,将所有标签保存在map中 for(Tag t: getTags()){ tagMap.put(t.getName(),t); } // 返回tagMap return tagMap; } }
重启faq项目
启动Nacosgatewayknows-client
再访问学生首页,第一次访问时控制台会输出"Redis加载所有标签完毕",但是之后的刷新就不会再输出这个信息了,甚至重启faq模块之后访问学生首页也不会输出这个信息了,原因是Redis中保存着这个信息,只要Redis不重启,就不会出现这个信息!
Ribbon实现微服务间调用 什么是RibbonRibbon也是SpringCloud提供的组件
它的功能是实现微服务之间的相互调用的
因为微服务项目每个业务都是项目整体的多个分支
分支之间一定会有交互,微服务之间的互相调用是普遍存在的
由此可知,Ribbon的使用时非常频繁和普遍呃,所以我们添加的
spring-cloud-starter-alibaba-nacos-discovery依赖已经包含了Ribbon,
也就是说Ribbon不需要单独添加配置和依赖,直接使用即可
Ribbon使用示例在使用Ribbon之前
我们需要先明确在指定的业务流程中,Ribbon作用的多个微服务项目中哪个是调用的发起者,哪个是被调用的
被调用的一方称之为服务的提供者,也叫"生产者"
发起调用的一方称之为服务的调用者,也叫"消费者"
Ribbon可以调用什么样的方法
Ribbon本质上就是向目标服务器发送了一次请求,它能调用到的方法就是目标服务器编写的控制器的方法,调用的依据就是控制器方法的url
我们将用于Ribbon调用的控制器方法称之为"Rest接口"
定义一个生产者的方法(Controller类的方法)
添加Ribbon调用的支持(每个微服务项目编写一次)
消费者发起调用(一般在业务逻辑层中发起调用)
步骤1:
定义生产者
任何已经定义好的控制器方法,都可以被Ribbon调用
我们现在将knows-sys模块的AuthController类中的demo方法作为生产者,也就是调用目标
调用它的路径就是:/v1/auth/demo
步骤2:
添加Ribbon的支持
我们需要在将要发起Ribbon请求的项目中添加Ribbon的支持
我们一般会在SpringBoot启动类中将能够调用Ribbon请求的对象保存在Spring容器中,以备项目使用
我们本次测试使用faq模块调用sys模块中的方法
knows-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
// LoadBalanced是负载均衡的意思
// 微服务模块间的调用是不经过网关的,所以网关中设置的负载均衡无效
// 导致Ribbon请求需要单独的配置负载均衡的注解,完成高效调用
@LoadBalanced
// 向Spring容器中保存一个RestTemplate类型的对象,支持Ribbon调用
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
RestTemplate就是能够执行Ribbon请求的对象
这个步骤只需要配置一次,如果在其他业务中knows-faq项目又需要调用其他项目的方法,就不需要再次配置了
步骤3:
发起调用
我们应该已经在发起调用的一方中配置了RestTemplate对象
实际开发中,都是在业务逻辑层中发起Ribbon调用
现在我们是测试,写在测试类中即可
调用目标:/v1/auth/demo
测试代码如下
// 从Spring容器中获得发起Ribbon请求的对象
@Resource
RestTemplate restTemplate;
@Test
public void ribbon(){
// 发送ribbon请求,先定义url
// url=[协议]+[服务器名称]+[控制器路径]
// 服务器名称必须是Nacos服务列表中存在的名称
// 控制器路径就是从/v1开始的路径
String url="http://sys-service/v1/auth/demo";
// 发起Ribbon请求
String str=restTemplate.getForObject(url,String.class);
System.out.println(str);
}
测试必须在Nacos和sys启动的情况下运行
faq模块会在测试运行时启动
Ribbon调用示意图
上面章节完成了第一个Ribbon程序
在实际开发中,需要更有意义的控制器方法作为调用目标,而不是返回一个"helloworld"
下面我们就来实现根据用户名获得用户对象的实际业务
这个业务在faq模块中,很多方法都需要使用
sys模块仍然是生产者
faq模块仍然是消费者
sys模块要定义一个根据用户名返回用户对象的控制层方法
因为没有这个方法,所以我们要
转到knows-sys模块,编写出这个方法
先编写业务逻辑层IUserService添加方法如下
// 根据用户名获得用户对象 User getUserByUsername(String username);
UserServiceImpl实现类方法
@Override
public User getUserByUsername(String username) {
return userMapper.findUserByUsername(username);
}
AuthController类中编写方法调用上面业务逻辑层
@Resource
private IUserService userService;
// 当前控制器方法用于Ribbon请求,路径设计为/v1/auth/user
// 参数是用户名,返回值为用户对象,Ribbon请求对应GetMapping
@GetMapping("/user")
public User getUser(String username){
return userService.getUserByUsername(username);
}
sys模块Rest接口定义完成
也就是生产者定义完成下面要开始编写消费者的Ribbon的支持
上次课已经配置完毕,直接跳过
所以直接开始在knows-faq模块编写Ribbon调用即可
仍然使用测试类来进行测试
// 根据用户名获得用户对象的Ribbon调用
@Test
public void getUser(){
// url路径中?之后的内容就是Ribbon请求的参数
// 参数名称必须和控制器方法参数名称一致
// 参数的值不直接赋值,使用{1}来占位
// 调用时有既定的赋值方式
String url="http://sys-service/v1/auth/user?username={1}";
// 调用时,前两个参数意义不变,第三参数向{1}中复制
User user=restTemplate
.getForObject(url,User.class,"st2");
System.out.println(user);
}
启动Nacos重启Sys后再运行测试
测试结果中包含我们查询的用户信息,表示一切正常
微服务的会话保持 会话和会话保持会话就是Session
指多次请求和响应的过程,只要浏览器打开之后不关闭,不超时就是同一次会话,在本次会话中登录成功时,会将用户信息保存在会话中,只要是同一次会话,当前登录的用户信息,就能一直保存在当前服务器内存中
我们达内知道单体项目portal实际上也是有会话支持的,只是所有会话的操作都封装在了Spring-Security框架,Spring-Security底层也是依靠session实现会话保持的
单体项目只需要一个服务器来保存当前登录用户信息,就可以实现会话保持
但是微服务项目是由多个服务器构成的,我们登录之后如何让所有微服务都知道我们的身份就成为了问题
微服务中的会话保持每个服务器都有自己的内存,它们的内存中的数据不会自动共享
当一个用户登录一台服务器后,相当于把自己的用户信息保存在了这台服务器的内存中,当访问其它服务器时,新的服务器并没有保存当前用户的信息,所以会话保持是失败的
所有微服务项目都面临这个问题
那么想要在微服务架构下实现登录后还能会话保持,就需要特殊的解决方案
这个方案的名称叫"单点登录"
单点登录的实现思路现在业界实现单点登录的思路主要有两种,都能够实现微服务的会话保持
1.Session共享
2.Token(令牌)
方案一:Session共享核心思想就是将登录成功的用户信息共享给所有模块
实现思路
用户在登录服务器(sys)模块登录成功,同时会将用户信息保存在Redis中(保存的key为当前用户的sessionId)
该用户访问其它模块时,这个模块会到Redis中寻找这个用户的信息(依据也是当前用户的sessionId),这样就能实现会话保持
优点:
支持Session共享功能的框架比较成熟,仅需要简单配置就可以实现功能结构相对简单,不需要大范围修改代码和更改程序结构,成本比较低
缺点:
因为用户信息要共享到Redis中,各个微服务模块需要时还需要从Redis中获取,内存使用开销较大,影响服务器性能只能在当前微服务项目中实现会话保持,比较难以实现跨项目的信息共享 方案二:Token令牌
登录成功时,由登录服务器向客户端响应一个Token(令牌),客户端来保存这个Token,这个Token就是一个加密的字符串,其中包含当前登录用户的信息
当前项目的所有微服务都可以解析这个Token
最终客户端需要表名自己用户的身份时,只需要将Token和自己的请求信息一起发送给服务器,任何模块的微服务都可以知道这个用户的身份信息
优点:
服务器内存不需要再保存用户信息,减少内存开销,运行效率更高客户端保存令牌,只要是可以解析这个令牌的项目都可以知道用户身份,方便跨应用(app)登录,也方便功能的扩展
缺点:
一般需要一个单独的授权服务器来生成和解析令牌,配置内容较多,开发成本高因为加密和解密需要CPU的参与,所以需要CPU的算力,消耗的额外的算力,CPU的运行效率可能受影响
达内知道项目使用Token令牌的方式实现微服务架构的单点登录功能
Oauth2概述 什么是Oauth2实现Token单点登录的解决方案现在业界是有明确标准的
Oauth(Open Auth:开放授权)是一套授权标准,是业界都在使用的一套完整的授权解决方案的格式
我们所使用的Oauth2.0就是Oauth1.0的升级.但是Oauth1.0基本没有被使用
现在很多大公司和企业都使用Oauth2作为开发授权标准
Oauth2支持的部分常用授权模式
扫码登录(专业名称:授权码登录)
用户名密码登录
客户端登录(手机内部的app授权)
…
Spring Cloud SecuritySpring Cloud Security就是微服务版的Spring-Security框架
在原有的基础上添加了微服务结构下用户的登录和权限管理的支持
最终我们要使用Spring Cloud Security和Oauth2结合实现微服务架构下的Token单点登录
最终实现项目结构如下图
微服务项目结构从功能上分为两大类
授权服务器:接收用户名和密码,验证登录,返回令牌资源服务器:当用户请求当前服务器时,解析令牌,获得用户信息
Oauth2标准主要使用在授权服务器中
我们会创建授权服务器项目,这个项目会添加Oauth2的支持
这个依赖中包含了很多标准的方法
主要是包含了很多控制器方法,也就是说,我们创建的auth模块是不需要编写控制器方法的,都是Oauth2提供的,我们需要做的就是对这个项目进行各种配置
创建Auth授权服务器项目创建knows-auth项目
父子相认
knows-auth
knows-auth的pom.xml
4.0.0 cn.tedu knows 0.0.1-SNAPSHOT cn.tedu knows-auth 0.0.1-SNAPSHOT knows-auth Demo project for Spring Boot org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery cn.tedu knows-commons org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java org.springframework.cloud spring-cloud-starter-security org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.security spring-security-jwt
application.properties文件配置如下
spring.datasource.url=jdbc:mysql://localhost:3306/knows?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true spring.datasource.username=root spring.datasource.password=root logging.level.cn.tedu.knows.auth=debug server.port=8010 spring.application.name=auth-service spring.cloud.nacos.discovery.server-addr=localhost:8848 # 下面的配置是允许Spring容器中已经存在的对象被新对象覆盖 # 意思就是两个相同id的对象保存到Spring容器时会不会报错 # 设置完true之后,相同id的后一个出现的对象会覆盖掉之前的对象 # 当前我们的auth项目内部,会有我们注入的对象覆盖系统原有对象的情况 spring.main.allow-bean-definition-overriding=true
SpringBoot启动类
@SpringBootApplication
@EnableDiscoveryClient
public class KnowsAuthApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsAuthApplication.class, args);
}
// Ribbon的支持
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
授权服务器配置准备
auth项目会配置很多信息
我们先按步骤进行准备工作
先配置Spring-Security放行
创建security包,包中创建SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 当前auth项目也是设置Spring-Security全部放行
// 因为登录验证交给了Oauth2,Spring-Security不在负责验证
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 关闭防跨域攻击
.authorizeRequests() // 设计访问权限
.anyRequest().permitAll() // 任何请求全部放行
.and().formLogin(); // 支持表单登录
}
// 我们在Spring容器中保存一个加密对象
// 之后有配置需要加密,就可以取出使用
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 我们在后面的配置中,需要Spring-Security框架中的授权管理器
// 授权管理器是登录功能的重要组成部分,现在是Oauth2需要它
// 我们需要将这个授权管理器保存到Spring容器中,以便Oauth2使用它
@Bean
public AuthenticationManager
authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
在授权过程中,令牌的生成方式和保存方式也是非常重要的部分
我们要编写一个类,专门配置令牌的保存
我们最终的目标效果是生成一个令牌保存到客户端
但是分步骤完成,先做一个简单的版本,暂时还是保存在内存中
创建TokenConfig类,来编写保存令牌的配置信息
// 只要是Spring的配置,就需要添加这个注解
@Configuration
public class TokenConfig {
// 配置保存令牌的策略对象到Spring容器
// 1.保存在内存中
// 2.生成令牌保存在客户端
// 先暂时保存在内存中
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
}
我们当前微服务版本的登录,仍然是SpringSecurity的实现思路,只是搭配了Oauth2的标准使用
就是portal项目中UserDetailsServiceImpl类中的登录配置类似
根据UserDetailsServiceImpl类中的业务,我们需要下面3个方法
1.根据用户名获得用户对象
2.根据用户id获得用户的所有角色
3.根据用户id获得用户的所有权限
因为这些方法都是应该有sys用户管理模块提供的
所以转到knows-sys模块编写上面3个方法
我们编写过1方法
2.3方法没有编写
从业务逻辑层开始
IUserService接口添加方法如下
// 根据用户id获得所有权限 ListgetPermissionsById(Integer id); // 根据用户id获得所有角色 List getRolesById(Integer id);
UserServiceImpl实现类
@Override public ListgetPermissionsById(Integer id) { return userMapper.findUserPermissionsById(id); } @Override public List getRolesById(Integer id) { return userMapper.findUserRolesById(id); }
AuthController类添加Rest接口
// 根据用户id获得所有权限
@GetMapping("/permissions")
public List permissions(Integer id){
return userService.getPermissionsById(id);
}
// 根据用户id获得所有角色
@GetMapping("/roles")
public List roles(Integer id){
return userService.getRolesById(id);
}
英文
incrincrement:增长
随笔 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"



