开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

MySQL查询优化(三):深度解读 MySQL客户端和服务端

发表于 2021-04-17

如果需要从 MySQL 服务端获得很高的性能,最佳的方式就是花时间研究 MySQL 优化和执行查询的机制。一旦理解了这些,大部分的查询优化是有据可循的,从而使得整个查询优化的过程更有逻辑性。下图展示了 MySQL 执行查询的过程:

  1. 客户端将 SQL 语句发送到服务端。
  2. 服务端检查查询缓存。如果缓存中已有数据,则直接返回缓存结果;否则,将 SQL 语句传递给下一环节。
  3. 服务端解析、预处理和优化 SQL 语句后,传递到查询优化器中形成查询计划。
  4. 查询执行引擎通过调用存储引擎接口执行查询计划。
  5. 服务端将查询结果返回给客户端。

上述的几个步骤都有其复杂性,接下来几篇文章将详细讲述各个环节。查询优化过程尤其复杂,并且理解这一环节很重要。
mysql 查询完整过程

MySQL 客户端/服务端协议

虽然并不需要了解 MySQL 客户端/服务端协议的内部细节,但需要从高应用层面理解其是如何工作的。这个协议是半双工的,这意味着 MySQL 服务端不同同时发送和接收消息,以及不可以将消息拆成多条短消息发送。这种机制一方面使得 MySQL 的通信简单快速,另一方面也增加了一些限制。例如,这意味着无法进行流控,一旦一方发送了消息,另一方在响应前必须接收整个消息。这就好像来回打乒乓球一样,同一时间只有一方有球,只有接到了球才能把它打回去。

客户端通过单个数据包将查询语句发送给服务端,因此在存在大的查询语句时配置 max_allowed_packet 很重要。一旦客户端发送查询语句后,它就只能等待返回结果。

相反,服务端的响应通常是由多个数据包组成的。一旦服务端响应后,客户端必须获取整个结果集。客户端没法简单地获取几行然后告诉服务端不要再发送剩余的数据。如果客户端仅仅需要返回数据前面的几行,只能是等待服务端全部数据返回后再从中丢弃不需要的数据,或者是粗暴地断开连接。不管哪种方式都不是好的选择,因此合适的 LIMIT子句就显得十分重要。

大部分的 MySQL连接库支持获取整个结果集并在内存中缓存起来,或者是获取需要的数据行。默认的行为通常是获取整个结果集然后在内存缓存。知道这一点很重要,因为 MySQL 服务端在所有请求的数据行没返回前,不会释放这次查询的锁和资源。大部分客户端库会让你感觉数据是从服务端获取的,实际上这些数据可能仅仅是从缓存中读取的。这在大部分时间是没问题的,但对于耗时很久或占据很多内存的大数据量查询来说就不合适了。如果指定了不缓存查询结果,那么占用的内存会更小,并且可以更快地处理结果。缺点是这种方式会在查询时引起
服务端的锁和资源占用。

以 PHP 为例,以下是PHP常用的查询代码:

1
2
3
4
5
6
7
8
php复制代码<?php
$link = mysql_connect('localhost', 'user', 'password');
$result = mysql_query('SELECT * FROM huge_table', $link);
while ($row = mysql_fetch_array($result)) {
//处理数据结果
}

?>

这个代码看起来好像是只获取了需要的数据行。然而,这个查询通过 mysql_query 的调用后实际上将全部结果放到了内存中。而 while 循环实际上是对内存中的数据进行循环迭代。相反,如果使用 mysql_unbuffered_query 替代 mysql_query 的话,那就不会缓存结果。

1
2
3
4
5
6
7
8
php复制代码<?php
$link = mysql_connect('localhost', 'user', 'password');
$result = mysql_unbuffered_query('SELECT * FROM huge_table', $link);
while ($row = mysql_fetch_array($result)) {
//处理数据结果
}

?>

不同的编程语言处理缓存覆盖的方式不同。例如,Perl 的 DBD::mysql 驱动需要通过 mysql_use_result 属性指定 C 语音客户端库(默认是 mysql_buffer_result),示例如下:

1
2
3
4
5
6
7
8
9
perl复制代码#!/usr/bin/perl

use DBI;
my $dbn = DBI->connect('DBI:mysql:;host=localhost', 'user', 'password');
my $sth = $dbn->prepare('SELECT * FROM huge_table', {mysql_use_result => 1});
$sth->execute();
while (my $row = $sth->fetchrow_array()) {
#处理数据结果
}

注意到 prepare 指定了使用结果而不是缓存结果。也可以通过在连接的时候指定,这会使得每次查询都不缓存。

1
perl复制代码my $dbn = DBI->connect('DBI:mysql:;mysql_use_result=1;host=localhost', 'user', 'password');

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

看一遍就理解:动态规划详解

发表于 2021-04-17

前言

我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~

  • 什么是动态规划?
  • 动态规划的核心思想
  • 一个例子走进动态规划
  • 动态规划的解题套路
  • leetcode案例分析

公众号:捡田螺的小男孩

什么是动态规划?

动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。

dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.

以上定义来自维基百科,看定义感觉还是有点抽象。简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。

一般这些子问题很相似,可以通过函数关系式递推出来。然后呢,动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就可以看做入门级的经典动态规划问题。

动态规划核心思想

动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算。

动态规划在于记住过往

我们来看下,网上比较流行的一个例子:

  • A : “1+1+1+1+1+1+1+1 =?”
  • A : “上面等式的值是多少”
  • B : 计算 “8”
  • A : 在上面等式的左边写上 “1+” 呢?
  • A : “此时等式的值为多少”
  • B : 很快得出答案 “9”
  • A : “你怎么这么快就知道答案了”
  • A : “只要在8的基础上加1就行了”
  • A : “所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 ‘记住求过的解来节省时间’”

一个例子带你走进动态规划 – 青蛙跳阶问题

暴力递归

leetcode原题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。

有些小伙伴第一次见这个题的时候,可能会有点蒙圈,不知道怎么解决。其实可以试想:

  • 要想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。
  • 同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。
  • 要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去。

假设跳到第n级台阶的跳数我们定义为f(n),很显然就可以得出以下公式:

1
2
3
4
5
6
7
scss复制代码f(10) = f(9)+f(8)
f (9) = f(8) + f(7)
f (8) = f(7) + f(6)
...
f(3) = f(2) + f(1)

即通用公式为: f(n) = f(n-1) + f(n-2)

那f(2) 或者 f(1) 等于多少呢?

  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;
  • 当只有1级台阶时,只有一种跳法,即f(1)= 1;

因此可以用递归去解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
scss复制代码class Solution {
public int numWays(int n) {
if(n == 1){
return 1;
}
if(n == 2){
return 2;
}
return numWays(n-1) + numWays(n-2);
}
}

去leetcode提交一下,发现有问题,超出时间限制了

为什么超时了呢?递归耗时在哪里呢?先画出递归树看看:

  • 要计算原问题 f(10),就需要先计算出子问题 f(9) 和 f(8)
  • 然后要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推。
  • 一直到 f(2) 和 f(1),递归树才终止。

我们先来看看这个递归的时间复杂度吧:

1
复制代码递归时间复杂度 = 解决一个子问题时间*子问题个数
  • 一个子问题时间 = f(n-1)+f(n-2),也就是一个加法的操作,所以复杂度是 O(1);
  • 问题个数 = 递归树节点的总数,递归树的总节点 = 2^n-1,所以是复杂度O(2^n)。

因此,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增长的,如果n比较大的话,超时很正常的了。

回过头来,你仔细观察这颗递归树,你会发现存在大量重复计算,比如f(8)被计算了两次,f(7)被重复计算了3次…所以这个递归算法低效的原因,就是存在大量的重复计算!

既然存在大量重复计算,那么我们可以先把计算好的答案存下来,即造一个备忘录,等到下次需要的话,先去备忘录查一下,如果有,就直接取就好了,备忘录没有才开始计算,那就可以省去重新重复计算的耗时啦!这就是带备忘录的解法。

带备忘录的递归解法(自顶向下)

一般使用一个数组或者一个哈希map充当这个备忘录。

  • 第一步,f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中,如下:

  • 第二步, f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中~

第三步, f(8) = f(7)+ f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以剪掉。

所以呢,用了备忘录递归算法,递归树变成光秃秃的树干咯,如下:

带备忘录的递归算法,子问题个数=树节点数=n,解决一个子问题还是O(1),所以带备忘录的递归算法的时间复杂度是O(n)。接下来呢,我们用带备忘录的递归算法去撸代码,解决这个青蛙跳阶问题的超时问题咯~,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码public class Solution {
//使用哈希map,充当备忘录的作用
Map<Integer, Integer> tempMap = new HashMap();
public int numWays(int n) {
// n = 0 也算1种
if (n == 0) {
return 1;
}
if (n <= 2) {
return n;
}
//先判断有没计算过,即看看备忘录有没有
if (tempMap.containsKey(n)) {
//备忘录有,即计算过,直接返回
return tempMap.get(n);
} else {
// 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中,对1000000007取余(这个是leetcode题目规定的)
tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);
return tempMap.get(n);
}
}
}

去leetcode提交一下,如图,稳了:

其实,还可以用动态规划解决这道题。

自底向上的动态规划

动态规划跟带备忘录的递归解法基本思想是一致的,都是减少重复计算,时间复杂度也都是差不多。但是呢:

  • 带备忘录的递归,是从f(10)往f(1)方向延伸求解的,所以也称为自顶向下的解法。
  • 动态规划从较小问题的解,由交叠性质,逐步决策出较大问题的解,它是从f(1)往f(10)方向,往上推求解,所以称为自底向上的解法。

动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。在青蛙跳阶问题中:

  • f(n-1)和f(n-2) 称为 f(n) 的最优子结构
  • f(n)= f(n-1)+f(n-2)就称为状态转移方程
  • f(1) = 1, f(2) = 2 就是边界啦
  • 比如f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题。

我们来看下自底向上的解法,从f(1)往f(10)方向,想想是不是直接一个for循环就可以解决啦,如下:

带备忘录的递归解法,空间复杂度是O(n),但是呢,仔细观察上图,可以发现,f(n)只依赖前面两个数,所以只需要两个变量a和b来存储,就可以满足需求了,因此空间复杂度是O(1)就可以啦

动态规划实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码public class Solution {
public int numWays(int n) {
if (n<= 1) {
return 1;
}
if (n == 2) {
return 2;
}
int a = 1;
int b = 2;
int temp = 0;
for (int i = 3; i <= n; i++) {
temp = (a + b)% 1000000007;
a = b;
b = temp;
}
return temp;
}
}

动态规划的解题套路

什么样的问题可以考虑使用动态规划解决呢?

如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。

比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。

动态规划的解题思路

动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。 并且动态规划一般都是自底向上的,因此到这里,基于青蛙跳阶问题,我总结了一下我做动态规划的思路:

  • 穷举分析
  • 确定边界
  • 找出规律,确定最优子结构
  • 写出状态转移方程

1. 穷举分析

  • 当台阶数是1的时候,有一种跳法,f(1) =1
  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;
  • 当台阶是3级时,想跳到第3级台阶,要么是先跳到第2级,然后再跳1级台阶上去,要么是先跳到第 1级,然后一次迈 2 级台阶上去。所以f(3) = f(2) + f(1) =3
  • 当台阶是4级时,想跳到第3级台阶,要么是先跳到第3级,然后再跳1级台阶上去,要么是先跳到第 2级,然后一次迈 2 级台阶上去。所以f(4) = f(3) + f(2) =5
  • 当台阶是5级时……

自底向上的动态规划

2. 确定边界

通过穷举分析,我们发现,当台阶数是1的时候或者2的时候,可以明确知道青蛙跳法。f(1) =1,f(2) = 2,当台阶n>=3时,已经呈现出规律f(3) = f(2) + f(1) =3,因此f(1) =1,f(2) = 2就是青蛙跳阶的边界。

3. 找规律,确定最优子结构

n>=3时,已经呈现出规律 f(n) = f(n-1) + f(n-2) ,因此,f(n-1)和f(n-2) 称为 f(n) 的最优子结构。什么是最优子结构?有这么一个解释:

一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质

4, 写出状态转移方程

通过前面3步,穷举分析,确定边界,最优子结构,我们就可以得出状态转移方程啦:

5. 代码实现

我们实现代码的时候,一般注意从底往上遍历哈,然后关注下边界情况,空间复杂度,也就差不多啦。动态规划有个框架的,大家实现的时候,可以考虑适当参考一下:

1
2
3
4
5
6
7
8
9
scss复制代码dp[0][0][...] = 边界值
for(状态1 :所有状态1的值){
for(状态2 :所有状态2的值){
for(...){
//状态转移方程
dp[状态1][状态2][...] = 求最值
}
}
}

leetcode案例分析

我们一起来分析一道经典leetcode题目吧

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

示例 1:

1
2
3
ini复制代码输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

1
2
ini复制代码输入:nums = [0,1,0,3,2,3]
输出:4

我们按照以上动态规划的解题思路,

  • 穷举分析
  • 确定边界
  • 找规律,确定最优子结构
  • 状态转移方程

1.穷举分析

因为动态规划,核心思想包括拆分子问题,记住过往,减少重复计算。 所以我们在思考原问题:数组num[i]的最长递增子序列长度时,可以思考下相关子问题,比如原问题是否跟子问题num[i-1]的最长递增子序列长度有关呢?

自顶向上的穷举

这里观察规律,显然是有关系的,我们还是遵循动态规划自底向上的原则,基于示例1的数据,从数组只有一个元素开始分析。

  • 当nums只有一个元素10时,最长递增子序列是[10],长度是1.
  • 当nums需要加入一个元素9时,最长递增子序列是[10]或者[9],长度是1。
  • 当nums再加入一个元素2时,最长递增子序列是[10]或者[9]或者[2],长度是1。
  • 当nums再加入一个元素5时,最长递增子序列是[2,5],长度是2。
  • 当nums再加入一个元素3时,最长递增子序列是[2,5]或者[2,3],长度是2。
  • 当nums再加入一个元素7时,,最长递增子序列是[2,5,7]或者[2,3,7],长度是3。
  • 当nums再加入一个元素101时,最长递增子序列是[2,5,7,101]或者[2,3,7,101],长度是4。
  • 当nums再加入一个元素18时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4。
  • 当nums再加入一个元素7时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4.
分析找规律,拆分子问题

通过上面分析,我们可以发现一个规律:

如果新加入一个元素nums[i], 最长递增子序列要么是以nums[i]结尾的递增子序列,要么就是nums[i-1]的最长递增子序列。看到这个,是不是很开心,nums[i]的最长递增子序列已经跟子问题 nums[i-1]的最长递增子序列有关联了。

1
css复制代码原问题数组nums[i]的最长递增子序列 = 子问题数组nums[i-1]的最长递增子序列/nums[i]结尾的最长递增子序列

是不是感觉成功了一半呢?但是如何把nums[i]结尾的递增子序列也转化为对应的子问题呢?要是nums[i]结尾的递增子序列也跟nums[i-1]的最长递增子序列有关就好了。又或者nums[i]结尾的最长递增子序列,跟前面子问题num[j](0=<j<i)结尾的最长递增子序列有关就好了,带着这个想法,我们又回头看看穷举的过程:

nums[i]的最长递增子序列,不就是从以数组num[i]每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,所以原问题,我们转化成求出以数组nums每个元素结尾的最长子序列集合,再取最大值嘛。哈哈,想到这,我们就可以用dp[i]表示以num[i]这个数结尾的最长递增子序列的长度啦,然后再来看看其中的规律:

其实,nums[i]结尾的自增子序列,只要找到比nums[i]小的子序列,加上nums[i] 就可以啦。显然,可能形成多种新的子序列,我们选最长那个,就是dp[i]的值啦

  • nums[3]=5,以5结尾的最长子序列就是[2,5],因为从数组下标0到3遍历,只找到了子序列[2]比5小,所以就是[2]+[5]啦,即dp[4]=2
  • nums[4]=3,以3结尾的最长子序列就是[2,3],因为从数组下标0到4遍历,只找到了子序列[2]比3小,所以就是[2]+[3]啦,即dp[4]=2
  • nums[5]=7,以7结尾的最长子序列就是[2,5,7]和[2,3,7],因为从数组下标0到5遍历,找到2,5和3都比7小,所以就有[2,7],[5,7],[3,7],[2,5,7]和[2,3,7]这些子序列,最长子序列就是[2,5,7]和[2,3,7],它俩不就是以5结尾和3结尾的最长递增子序列+[7]来的嘛!所以,dp[5]=3 =dp[3]+1=dp[4]+1。

很显然有这个规律:一个以nums[i]结尾的数组nums

  • 如果存在j属于区间[0,i-1],并且num[i]>num[j]的话,则有,dp(i) =max(dp(j))+1,

最简单的边界情况

当nums数组只有一个元素时,最长递增子序列的长度dp(1)=1,当nums数组有两个元素时,dp(2) =2或者1,
因此边界就是dp(1)=1。

确定最优子结构

从穷举分析,我们可以得出,以下的最优结构:

1
scss复制代码dp(i) =max(dp(j))+1,存在j属于区间[0,i-1],并且num[i]>num[j]。

max(dp(j)) 就是最优子结构。

状态转移方程

通过前面分析,我们就可以得出状态转移方程啦:

所以数组num[i]的最长递增子序列就是:

1
scss复制代码最长递增子序列 =max(dp[i])

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ini复制代码class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
//初始化就是边界情况
dp[0] = 1;
int maxans = 1;
//自底向上遍历
for (int i = 1; i < nums.length; i++) {
dp[i] = 1;
//从下标0到i遍历
for (int j = 0; j < i; j++) {
//找到前面比nums[i]小的数nums[j],即有dp[i]= dp[j]+1
if (nums[j] < nums[i]) {
//因为会有多个小于nums[i]的数,也就是会存在多种组合了嘛,我们就取最大放到dp[i]
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
//求出dp[i]后,dp最大那个就是nums的最长递增子序列啦
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}

参考与感谢

  • leetcode官网
  • 《labuladong算法小抄》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点认证框架 简单过一下 Shiro

发表于 2021-04-16

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

之前说了 SpringSecurity , 也说了 Pac4j , 后续准备把 Shiro 和 CAS 也完善进来 , Shiro 整个框架结构比较简单 , 这一篇也只是简单过一下 , 不深入太多.

1.1 基础知识

Shiro 的基础知识推荐看官方文档 Shiro Doc , 这里就简单的罗列一下

Shiro 具有很简单的体系结构 (Subject,SecurityManager 和 Realms) , 按照流程大概就是这样

1
2
3
4
5
java复制代码ApplicationCode -->  Subject (Current User)
|
SecurityManager (Managers all Subject)
|
Realms

Shiro 的基石

Shiro 自己内部定义了4个功能基石 , 分为身份验证、授权、会话管理和密码学

  • Authentication : 身份认证 , 证明用户身份的行为
  • Authorization : 访问控制的过程,即确定谁可以访问什么
  • Session Management : 管理特定于用户的会话,即使是在非 web 或 EJB 应用程序中
  • Cryptography : 使用加密算法来保证数据的安全,同时仍然易于使用

image.png

image.png

以及一些额外的功能点:

  • Web Support : Shiro 的 Web 支持 api 帮助简单地保护 Web 应用程序
  • Caching : 缓存是 Apache Shiro API 中的第一层,用于确保安全操作保持快速和高效
  • Concurrency : Apache Shiro 支持多线程应用程序及其并发特性
  • Run As : 允许用户假设另一个用户的身份的特性 (我理解这就是代办)
  • Remember Me : 记住我功能

补充隐藏概念:

  • Permission : 许可
  • Role : 角色

二 . 基本使用

Shiro 的使用对我而言第一感觉就是干净 , 你不需要像 SpringSecurity 一样去关注很多配置 ,关注很多Filter , 也不需要像 CAS 源码一样走了很多 WebFlow , 所有的认证都是由你自己去完成的.

2.1 配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
java复制代码@Configuration
public class shiroConfig {


/**
* 配置 Realm
*
* @return
*/
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
return customRealm;
}

/**
* 权限管理,配置主要是Realm的管理认证
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}

//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
// logout url
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}

/**
* 注册 SecurityManager
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}


/**
* AOP 注解冲突解决方式
*
* @return
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
}

2.2 发起认证

Shiro 发起认证很简答 , 完全是手动发起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码     Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUserName(),
user.getPassword()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
// subject.checkRole("admin");
// subject.checkPermissions("query", "add");
} catch (UnknownAccountException e) {
log.error("用户名不存在!", e);
return "用户名不存在!";
} catch (AuthenticationException e) {
log.error("账号或密码错误!", e);
return "账号或密码错误!";
} catch (AuthorizationException e) {
log.error("没有权限!", e);
return "没有权限";
}

因为是完全手动发起的 , 所以在集成 Shiro 的时候毫无压力 , 可以自行在外层封装任何的接口 , 也可以在接口中做任何的事情.

2.3 校验逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
java复制代码public class CustomRealm extends AuthorizingRealm {

@Autowired
private LoginService loginService;

/**
* @MethodName doGetAuthorizationInfo
* @Description 权限配置类
* @Param [principalCollection]
* @Return AuthorizationInfo
* @Author WangShiLin
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = loginService.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (Permissions permissions : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
}
}
return simpleAuthorizationInfo;
}

/**
* @MethodName doGetAuthenticationInfo
* @Description 认证配置类
* @Param [authenticationToken]
* @Return AuthenticationInfo
* @Author WangShiLin
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = loginService.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(), getName());
return simpleAuthenticationInfo;
}
}
}

LoginServiceImpl 也简单贴一下 , 就是从数据源中获取用户而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
java复制代码@Service
public class LoginServiceImpl implements LoginService {

@Autowired
private PermissionServiceImpl permissionService;

@Override
public User getUserByName(String getMapByName) {
return getMapByName(getMapByName);
}

/**
* 模拟数据库查询
*
* @param userName 用户名
* @return User
*/
private User getMapByName(String userName) {

// 构建 Role 1
Role role = new Role("1", "admin", getAllPermission());
Set<Role> roleSet = new HashSet<>();
roleSet.add(role);

// 构建 Role 2
Role role1 = new Role("2", "user", getSinglePermission());
Set<Role> roleSet1 = new HashSet<>();
roleSet1.add(role1);

User user = new User("1", "root", "123456", roleSet);
Map<String, User> map = new HashMap<>();
map.put(user.getUserName(), user);

User user1 = new User("2", "zhangsan", "123456", roleSet1);
map.put(user1.getUserName(), user1);

return map.get(userName);
}

/**
* 权限类型一
*/
private Set<Permissions> getAllPermission() {
Set<Permissions> permissionsSet = new HashSet<>();
permissionsSet.add(permissionService.getPermsByUserId("1"));
permissionsSet.add(permissionService.getPermsByUserId("2"));
return permissionsSet;
}

/**
* 权限类型二
*/
private Set<Permissions> getSinglePermission() {
Set<Permissions> permissionsSet1 = new HashSet<>();
permissionsSet1.add(permissionService.getPermsByUserId("1"));
return permissionsSet1;
}

}

LoginServiceImpl其实都可以不算是 Shiro 整个认证体系的一员 ,它只是做一个 User 管理的业务而已 , 那么剩下了干了什么?

  • 写了一个 API 接口
  • 准备了一个 Realm
  • 通过 Subject 发起认证
  • 在接口上标注相关的注解

整套流程下来 , 就是简单 , 便捷 , 很轻松的就集成了认证的功能.

三 . 源码

按照惯例 , 还是看一遍源码吧 ,我们按照四个维度来分析 :

  • 请求的拦截
  • 请求的校验
  • 认证的过程
  • 退出的过程

3.1 请求的拦截

这要从 ShiroFilterFactoryBean 这个类开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
JAVA复制代码    @Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//....
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}


C- ShiroFilterFactoryBean
?- 构建 ShiroFilterFactoryBean 时会为其配置一个 login 地址
M- applyGlobalPropertiesIfNecessary : 配置全局属性
- applyLoginUrlIfNecessary(filter);
- applySuccessUrlIfNecessary(filter);
- applyUnauthorizedUrlIfNecessary(filter);

M- applyLoginUrlIfNecessary
?- 为 Filter 配置 loginUrl
- String existingLoginUrl = acFilter.getLoginUrl();
- acFilter.setLoginUrl(loginUrl)





// 这里对所有的 地址做了拦截
C01- PathMatchingFilter
F- protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();
?- 所有的path均会在这里处理
- 拦截成功了会调用 isFilterChainContinued , 最终会调用 onAccessDenied -> M2_01

C02- FormAuthenticationFilter
M2_01- onAccessDenied
-

// 判断是否需要重定向
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return executeLogin(request, response);
} else {
return true;
}
} else {
// 重定向到 login 页
saveRequestAndRedirectToLogin(request, response);
return false;
}
}

// 当然还有已登录得逻辑 , 已登录是在上面之前判断得
C- AccessControlFilter
M- onPreHandle
?- 该方法中会调用其他得 Filter 判断是否登录

// 例如这里就是 AuthenticationFilter 获取 Subject
C- AuthenticationFilter
M- isAccessAllowed
- Subject subject = getSubject(request, response);
- return subject.isAuthenticated() && subject.getPrincipal() != null;

整体的调用链大概是

  • OncePerRequestFilter # doFilter
  • AbstractShiroFilter # call
  • AbstractShiroFilter # executeChain
  • ProxiedFilterChain # doFilter
  • AdviceFilter # doFilterInternal
  • PathMatchingFilter # preHandle

最终会因为Filter 链 , 最终由 FormAuthenticationFilter 重定向出去

3.2 拦截的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码
按照我们的常规思路 , 拦截仍然是通过 Filter 来完成


C- AbstractShiroFilter
M- doFilterInternal
- final Subject subject = createSubject(request, response) : 通过 请求构建了一个 Subject
- 调用 Subject 的 Callable 回调
- updateSessionLastAccessTime(request, response);
- executeChain(request, response, chain);
M- executeChain
- 执行 FilterChain 判断
-

// 这里往上追溯 , 可以看到实际上是一个 AOP 操作 : ReflectiveMethodInvocation
// 再往上就是 AopAllianceAnnotationsAuthorizingMethodInterceptor , 注意这里面是懒加载的

C03- AnnotationsAuthorizingMethodInterceptor : 通过 Interceptor 对方法进行拦截
M3_01- assertAuthorized : 断言认证信息
- 获取一个集合 Collection<AuthorizingAnnotationMethodInterceptor>
FOR- 循环 AuthorizingAnnotationMethodInterceptor -> PS301
- assertAuthorized -> M3_05

C- AuthorizingAnnotationMethodInterceptor
M3_05- assertAuthorized(MethodInvocation mi)
- ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi))
- 这里是获取 Method 上面的 Annotation , 再调用 assertAuthorized 验证 -> M5_01

// 补充 : PS301
TODO

C05- RoleAnnotationHandler
M5_01- assertAuthorized(Annotation a)
- 如果不是 RequiresRoles , 则直接返回
- getSubject().checkRole(roles[0]) -> M6_02
- getSubject().checkRoles(Arrays.asList(roles));
?- 注意 , 这里是区别 And 和 Or 将 roles 分别处理

请求的逻辑 :

M3_01M3_05M5_01M6_02M10_05M11_04M11_05
AuthorizingAnnotationMethodInterceptor.png

Shiro001.jpg

3.3 一个完整的认证过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUserName(),user.getPassword());

subject.login(usernamePasswordToken);

// 来看一下 , 整个流程中做了什么
C06- DelegatingSubject
M6_01- login(AuthenticationToken token)
- Subject subject = securityManager.login(this, token) : 调用 securityManager 完成认证 :M8_01
M6_02- checkRole(String role)
- securityManager.checkRole(getPrincipals(), role) -> M10_04


C07- DefaultWebSecurityManager
M7_01-

C08- DefaultSecurityManager
M8_01- login(Subject subject, AuthenticationToken token)
- 底层调用 AuthorizingRealm 完成认证 , 此处的认证类为自定义的 CustomRealm

C09- AbstractAuthenticator
M9_01- authenticate(AuthenticationToken token)
M9_02-

C10- ModularRealmAuthenticator
M10_01- doAuthenticate(AuthenticationToken authenticationToken)
- 根据 Realm 数量 , 选择不同的认证类
- doSingleRealmAuthentication(realms.iterator().next(), authenticationToken) -> M10_02
- doMultiRealmAuthentication(realms, authenticationToken) -> M10_03
M10_02- doSingleRealmAuthentication
- AuthenticationInfo info = realm.getAuthenticationInfo(token)
M10_03- doMultiRealmAuthentication
M10_04- checkRole
- hasRole(principals, role) 判断是否 -> M10_05
M10_05- hasRole
FOR- getRealms() : 获取当前的 realms 类
- ((Authorizer) realm).hasRole(principals, roleIdentifier) : 调用 Reamlm 判断是否有 Role -> M11_04

C11- AuthenticatingRealm
M11_01- getAuthenticationInfo(AuthenticationToken token)
- getCachedAuthenticationInfo(token) : 获取缓存的 Authentication
- 调用 doGetAuthenticationInfo 进行实际的认证 : M12_02
- cacheAuthenticationInfoIfPossible 缓存认证信息
M11_02- cacheAuthenticationInfoIfPossible
- getAvailableAuthenticationCache() : 获取缓存集合
- getAuthenticationCacheKey(token) : 获取缓存 key
- cache.put(key, info) : 添加缓存
M11_03- assertCredentialsMatch
- CredentialsMatcher cm = getCredentialsMatcher();
- cm.doCredentialsMatch(token, info)
M11_04- hasRole
- AuthorizationInfo info = getAuthorizationInfo(principal)
-> M11_05
- hasRole(roleIdentifier, info)
M11_05- getAuthorizationInfo(PrincipalCollection principals)
?- 注意这里和 M11_01 的参数是不一样的
- getAvailableAuthorizationCache() 获取 Cache<Object, AuthorizationInfo>
- 如果 Cache 不为空 , getAuthorizationCacheKey 获取 key 后通过该key 从 Cache 里面获取 AuthorizationInfo
- 如果 缓存获取失败 , 则调用 doGetAuthorizationInfo(PrincipalCollection principals) 获取对象
?- 注意 , 这里要为其添加 Role -> M12_01



C12- CustomRealm
M12_01- AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)

M12_02- AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)

3.4 Logout 的流程

logout 中做了哪些事 ?

logout 最终会调用 subject logout 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码    public void logout() {
try {
// 本质上是从 Session 中移除 .RUN_AS_PRINCIPALS_SESSION_KEY
clearRunAsIdentitiesInternal();
this.securityManager.logout(this);
} finally {
// 把 Subject
this.session = null;
this.principals = null;
this.authenticated = false;
}
}

C- DefaultSecurityManager
M- logout
- beforeLogout(subject)
- subject.getPrincipals() 获取一个 PrincipalCollection
- 再获取一个 Authenticator , 通过它来 onLogout PrincipalCollection
?- ((LogoutAware) authc).onLogout(principals)
?- 需要这一步是因为可能存在 缓存和多模块登录 , 需要同时退出
// 最后移除 session , 删除 subject
- delete(subject);
- this.subjectDAO.delete(subject)
- stopSession(subject);

Shiro 的 logout 看起来也很清晰 , session 一关 , subject 一删 , 完毕 .

甚至于都不用考虑是否需要重定向 , 一切都是业务自己决定.

3.5 补充一 : Shiro 的异常体系

很清晰 , Shiro 认证失败均会有响应的异常 , 由异常处理就可以决定业务的走向

Shiro-AccountException.png

3.6 补充二 : 细说 DefaultSubjectDAO

DefaultSubjectDAO 是其中一个比较重要的逻辑 , 它负责处理 Subject 的相关持久化 , 当然使用者中我们可以做一个自己的实现类来处理Subject 的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
private SessionStorageEvaluator sessionStorageEvaluator;

// 主要看一下其中的 CURD 操作
M- saveToSession(Subject subject)
- mergePrincipals(subject);
- mergeAuthenticationState(subject);

M- mergePrincipals
?- 将主体当前的Subject#getPrincipals()与可能在其中的任何元素合并到任何可用的会话
- currentPrincipals = subject.getPrincipals();
- Session session = subject.getSession(false);
- session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
- session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals)

// 不用多说什么 , 很清晰的就能看到 , 将 currentPrincipals 设置到了可用的 Session 中 , 也就是说 , Principals 其实是在 Session 中流转

M- mergeAuthenticationState
- session = subject.getSession();
- session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
- session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);

// 一样的 , 通过 Session 控制

3.7 Subject 的管理逻辑

之前看到 Subject 获取时 ,是通过 getSubject 获取的 , 看看这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
// 看了这个类大概就知道 , 为什么 shiro 支持多线程
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}

// PS : ThreadContext 是 Shiro 自己的工具类


// TODO : 这里先留一个小坑 , 多线程的处理逻辑还没有专门分析 , 后续进行补充

四 . 扩展- 自行定义一个 OAuth 流程

因为Shiro 的特性 , 所以 OAuth 模式实际上是集成了其他的包

参考自 @ www.e-learn.cn/topic/15938… , 这一节不全 , 建议参考原文

Maven : 不要求一定是他们 , 其他的OAuth 实现均可

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.resourceserver</artifactId>
<version>1.0.2</version>
</dependency>

其他的整体而言多的就是2个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
java复制代码    @RequestMapping("/authorize")
public Object authorize(Model model, HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
logger.info("------ > 第一步 进入验证申请", request.toString());
try {
logger.info("------ > 第二步 生成 OAuthAuthzRequest", request.toString());
OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);
//检查传入的客户端id是否正确
if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
logger.info("step 3 获取 subject---:{}", SecurityUtils.getSubject().toString());
Subject subject = SecurityUtils.getSubject();
//如果用户没有登录,跳转到登陆页面
if (!subject.isAuthenticated()) {
if (!login(subject, request)) {//登录失败时跳转到登陆页面
model.addAttribute("client",
clientService.findByClientId(oauthRequest.getClientId()));
return "oauth2login";
}
}
logger.info("step 4 获取 username---:{}", (String) subject.getPrincipal());
String username = (String) subject.getPrincipal();
//生成授权码
String authorizationCode = null;
//responseType目前仅支持CODE,另外还有TOKEN
String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
if (responseType.equals(ResponseType.CODE.toString())) {
OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
authorizationCode = oauthIssuerImpl.authorizationCode();
logger.info("step 5 step -- authorizationCode :{}", authorizationCode);
oAuthService.addAuthCode(authorizationCode, username);
}
//进行OAuth响应构建
OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
OAuthASResponse.authorizationResponse(request,
HttpServletResponse.SC_FOUND);
logger.info("step 5 step -- OAuthAuthorizationResponseBuilder :{}", builder);
//设置授权码
builder.setCode(authorizationCode);
//得到到客户端重定向地址
String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);

//构建响应
final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
//根据OAuthResponse返回ResponseEntity响应
HttpHeaders headers = new HttpHeaders();
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException e) {
//出错处理
logger.info("step 2 进入authorize OAuthAuthzRequest---:{}", request.toString());
String redirectUri = e.getRedirectUri();
if (OAuthUtils.isEmpty(redirectUri)) {
//告诉客户端没有传入redirectUri直接报错
return new ResponseEntity(
"OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
}
//返回错误消息(如?error=)
final OAuthResponse response =
OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND)
.error(e).location(redirectUri).buildQueryMessage();
HttpHeaders headers = new HttpHeaders();
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
}
}

private boolean login(Subject subject, HttpServletRequest request) {
if ("get".equalsIgnoreCase(request.getMethod())) {
return false;
}
String username = request.getParameter("username");
String password = request.getParameter("password");

if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return false;
}

UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return true;
} catch (Exception e) {
request.setAttribute("error", "登录失败:" + e.getClass().getName());
return false;
}
}

AccessToken

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
java复制代码@RequestMapping("/accessToken")
public HttpEntity token(HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
try {
//构建OAuth请求
OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);
logger.info("step 1 OAuthTokenRequest request---:{}", oauthRequest.toString());
//检查提交的客户端id是否正确
if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}

// 检查客户端安全KEY是否正确
if (!oAuthService.checkClientSecret(oauthRequest.getClientSecret())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
logger.info("step 1 authCode request---:{}",oauthRequest.getParam(OAuth.OAUTH_CODE));
String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);
// 检查验证类型,此处只检查AUTHORIZATION_CODE类型,其他的还有PASSWORD或REFRESH_TOKEN
if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
GrantType.AUTHORIZATION_CODE.toString())) {
if (!oAuthService.checkAuthCode(authCode)) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_GRANT)
.setErrorDescription("错误的授权码")
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
}

//生成Access Token
OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
final String accessToken = oauthIssuerImpl.accessToken();
logger.info("step 1 accessToken request---:{}",accessToken);
logger.info("step 1 username data---:{}", oAuthService.getUsernameByAuthCode(authCode));
oAuthService.addAccessToken(accessToken,
oAuthService.getUsernameByAuthCode(authCode));

//生成OAuth响应
OAuthResponse response = OAuthASResponse
.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken)
.setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
.buildJSONMessage();

//根据OAuthResponse生成ResponseEntity
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException e) {
//构建错误响应
OAuthResponse res = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
.buildJSONMessage();
return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
}
}

总结一下

简单点说 , 就是 Shiro 对 OAuth 没有支持 ,而想要获得 OAuth 能力 , 就自己定制 , 只是把 Shiro 当成一个内部 SSO , 获取用户信息即可

核心代码

1
2
java复制代码Subject subject = SecurityUtils.getSubject();
String username = (String) subject.getPrincipal();

总结

Shiro 这一篇也完了 , 真的很浅 ,没讲什么深入的东西, 一大原因是 Shiro 的定位就是大道至简 .

他只给你提供认证的能力 , 你也只需要把他当成一个内部 SSO , 通过相关方法认证 和 获取用户即可.

同时 ,他提供了细粒度的支持 , 与其他项目耦合低 , 我们曾经就在存在一个 认证框架的时候去集成他的 细粒度能力 , 因为它通过手动登录 , 基本上没什么冲突 , 也很好用.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

分享一款轻量简洁的全平台开源商城系统

发表于 2021-04-16

  大家好,我是为广大程序员兄弟操碎了心的小编,每天推荐一个小工具/源码,装满你的收藏夹,每天分享一个小技巧,让你轻松节省开发效率,实现不加班不熬夜不掉头发,是我的目标!

  今天小编推荐一个基于spring boot、vuejs、futter快速构建全平台开源商城系统,并提供api,后台管理,包含h5,微信小程序,android,ios应用程序完整方案。

开源协议

  使用 MIT 开源许可协议,任何人任何单位可以免费学习使用该商城和基于该项目开发搭建自己的商城系统

链接地址

  公众号【Github导航站】回复关键词【小铺】获取git地址

技术栈

  • 核心框架:Spring Boot
  • 数据库层:Spring data jpa
  • 数据库连接池:Druid
  • 缓存:Ehcache
  • 前端:后台管理基于element,手机端界面基于vant

功能列表

  • 基础模块
    • 部门管理
    • 用户管理
    • 角色管理
    • 菜单管理
    • 权限分配
    • 参数管理
    • 数据字典管理
    • 定时任务管理
    • 操作日志
    • 登录日志
    • cms内容管理
    • 消息管理:配置消息模板,发送短信,邮件消息
    • 基于idea插件的代码生成
  • 商城功能
    • 会员管理
    • 商品类别
    • 商品管理
    • 订单管理
    • 购物车
    • banner管理
    • 收藏列表
  • 手机端 -完整的商城购物功能

演示截图

结尾

  本期就分享到这里,我是小编南风吹,专注分享好玩有趣、新奇、实用的开源项目及开发者工具、学习资源!希望能与大家共同学习交流,欢迎关注我的公众号**【Github导航站】**。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Mysql学习总结

发表于 2021-04-16

1. 存储引擎

MySQL中的数据用各种不同的技术存储在文件(或者内存)中。这些技术中的每一种技术都使用不同的存储机制、索引技巧、锁定水平并且最终提供广泛的不同的功能和能力。通过选择不同的技术,Mysql能够获得额外的速度或者功能,从而改善应用的整体功能。

(1)InnoDB

  • InnoDB 是 MySQL 默认的事务型存储引擎,只要在需要它不支持的特性时,才考虑使用其他存储引擎。
  • InnoDB 采用 MVCC 来支持高并发,并且实现了四个标准隔离级别(未提交读、提交读、可重复读、可串行化)。其默认级别时可重复读(REPEATABLE READ),在可重复读级别下,通过 MVCC + Next-Key Locking 防止幻读。
  • 主索引时聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对主键查询有很高的性能。
  • InnoDB 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读,能够自动在内存中创建 hash 索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。
  • InnoDB 支持真正的在线热备份,MySQL 其他的存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合的场景中,停止写入可能也意味着停止读取。

(2)MyISAM

  • 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。
  • 提供了大量的特性,包括压缩表、空间数据索引等。
  • 不支持事务。
  • 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。
  • 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
  • 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。

(3)两种存储引擎的对比

  • 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。
  • 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
  • 外键:InnoDB 支持外键。
  • 备份:InnoDB 支持在线热备份。
  • 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
  • 其它特性:MyISAM 支持压缩表和空间数据索引。

2. 索引

MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。

MySQL中常用的索引在物理上分两类,B-树索引和哈希索引。
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。

优点:

  • 大大减少了服务器需要扫描的数据行数。
  • 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)。
  • 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)。

为什么要用索引?

先说一下磁盘IO,磁盘读取数据靠的是机械运动,每一次读取数据需要寻道、寻点、拷贝到内存三步操作。一次IO的时间平均是在9ms左右。听起来很快,但数据库百万级别的数据过一遍就达到了9000s,显然就是灾难级别的了。

考虑到磁盘IO是非常高昂的操作,计算机操作系统做了预读的优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。

每一次IO读取的数据我们称之为一页(page),具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO。

所以想要优化数据库查询,就要尽量减少磁盘的IO操作,所以就出现了索引。

(1)B+Tree 索引

BTree索引

BTree又叫多路平衡查找树,一颗m叉的BTree特性如下:

  • 树中每个节点最多包含m个孩子。
  • 除根节点与叶子节点外,每个节点至少有[ceil(m/2)]个孩子(ceil()为向上取整)。
  • 若根节点不是叶子节点,则至少有两个孩子。
  • 所有的叶子节点都在同一层。
  • 每个非叶子节点由n个key与n+1个指针组成,其中[ceil(m/2)-1] <= n <= m-1 。

image.png
这是一个3叉(只是举例,真实会有很多叉)的BTree结构图,每一个方框块我们称之为一个磁盘块或者叫做一个block块,这是操作系统一次IO往内存中读的内容,一个块对应四个扇区,紫色代表的是磁盘块中的数据key,黄色代表的是数据data,蓝色代表的是指针p,指向下一个磁盘块的位置。

每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。

B+Tree索引

B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。

image.png
非叶子节点只存储键值信息, 数据记录都存放在叶子节点中, 将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,所以B+Tree的高度可以被压缩到特别的低。

我们只需要进行三次的IO操作就可以从10亿条数据中找到我们想要的数据,比起最开始的百万数据9000秒好了很多。

而且在B+Tree上通常有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。所以我们除了可以对B+Tree进行主键的范围查找和分页查找,还可以从根节点开始,进行随机查找。

  • B+ Tree 索引是大多数 MySQL 存储引擎的默认索引类型。
  • 因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。
  • 因为 B+ Tree 的有序性,所以除了用于查找,还可以用于排序和分组。
  • 可以指定多个列作为索引列,多个索引列共同组成键。
  • 适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
主索引,也叫聚集索引(clustered index)

image.png
主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。

辅助索引(secondary index)

image.png
辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找,这个过程也被称作回表。

(2)哈希索引

哈希索引能以 O(1) 时间进行查找,但是失去了有序性:

  • 无法用于排序与分组;
  • 只支持精确查找,无法用于部分查找和范围查找。
    InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。

(3)全文索引

  • MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。
  • 查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
  • 全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。
  • InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
    例如对于SQL语句nickname like '%看风%' ,默认情况下,不会选择走nickname索引的,改写SQL为全文索引匹配的方式:match(nickname) against('看风')就会走全文索引。对于完全模糊匹配%xxx%查询的SQL可以通过全文索引提高效率。

(4)空间数据索引

MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。

  • 必须使用 GIS 相关的函数来维护数据。

3. 索引优化

(1)独立的列

在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。

(2)多列索引

在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。

(3)索引列的顺序

让选择性最强的索引列放在前面。

  • 索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,每个记录的区分度越高,查询效率也越高。

(4)前缀索引

对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。前缀长度的选取需要根据索引选择性来确定。

(5)覆盖索引

索引包含所有需要查询的字段的值。

具有以下优点:

  • 索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。
  • 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
  • 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。

4. 全表扫描与索引的对比

  • 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;
  • 对于中到大型的表,索引就非常有效;
  • 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
  • 为什么对于非常小的表,大部分情况下简单的全表扫描比建立索引更高效?*

如果一个表比较小,那么显然直接遍历表比走索引要快(因为需要回表)。条件是查询的数据不是索引的构成部分,否也不需要回表操作。其次,查询条件也不是主键,否则可以直接从聚簇索引中拿到数据。

5. explain

explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
image.png

(1)select_type

image.png

(2)table

要查询的表

(3)type

索引查询类型,从最好到最差依次是:system>const>eq_ref>ref>range>index>ALL。一般来说,保证查询至少达到range级别,最好能达到ref。
image.png
image.png

(4)possible_keys

显示可能应用到这张表中的索引,一个或多个。查询涉及到的字段若存在索引,则该索引将被列出,但不一定被查询实际使用

(5)key

  • 实际使用的索引,如果为NULL,则没使用索引
  • 查询中若使用了覆盖索引,该索引仅出现在key列表中
    image.png

(6)key_len

  • 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。在不损失精度的情况下,长度越短越好
  • key_len显示的值为索引字段的最大可能长度,并非实际使用长度,是根据表的定义计算得到,不是通过表内检索出的

(7)ref

显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值

(8)rows

根据表统计信息及索引选用情况,大致估算出找到所需记录需要读取的行数

(9)Extra

image.png

6. SQL查询优化

(1)减少请求的数据量

  • 只返回必要的列:最好不要使用 SELECT * 语句。
  • 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
  • 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。

(2)减少服务器端扫描的行数

最有效的方式是使用索引来覆盖查询

(3)切分大查询

一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。所以可以将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联。

这样做的好处有:

  • 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
  • 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
  • 减少锁竞争;
  • 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。
  • 查询本身效率也可能会有所提升。例如使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。

7. 事务

事务是指满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。

(1)ACID

  • Atomicity:原子性
    事务被视为不可分割的最小单元,事务的所有操作要么全部成功,要么全部失败回滚。
  • Consistency:一致性
    数据库在事务执行前后都保持一致性状态,在一致性状态下,所有事务对一个数据的读取结果都是相同的。
  • Isolation:隔离性
    一个事务所做的修改在最终提交以前,对其他事务是不可见的。
  • Durability:持久性
    一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢。
    image.png
    只有满足一致性,事务的结果才是正确的。在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。事务满足持久化是为了能应对数据库崩溃的情况。

(2)隔离级别

  • 未提交读(READ UNCOMMITTED)
    事务中的修改,即使没有提交,对其他事务也是可见的。
  • 提交读(READ COMMITTED)
    一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其他事务是不可见的。
  • 可重复读(REPEATABLE READ)
    保证在同一个事务中多次读取同样数据的结果是一样的。
  • 可串行化(SERIALIZABLE)
    强制事务串行执行。需要加锁实现,而其它隔离级别通常不需要。

8. 锁

锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。

(1)锁类型

  • 共享锁(S Lock) :允许事务读一行数据
  • 排他锁(X Lock) :允许事务删除或者更新一行数据
  • 意向共享锁(IS Lock) :事务想要获得一张表中某几行的共享锁
  • 意向排他锁 :事务想要获得一张表中某几行的排他锁

(2)锁算法

在 InnoDB 存储引擎中,SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决,INSERT 的不可重复读问题是通过 Next-Key Lock(Record Lock + Gap Lock)解决的。

  • Record Lock
    锁定一个记录上的索引,而不是记录本身。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
  • Gap Lock
    锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
Next-Key Lock
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。

(3)锁问题

  • 脏读 : 不同事务下,当前事务可以读取到另外事务未提交的数据。
  • 不可重复读 : 同一事务内多次读取同一数据集合,读取到的数据是不一样的情况。
  • 幻读 : 在同一事务下,连续执行两次同样的 sql 语句可能返回不同的结果,第二次的 sql 语句可能会返回之前不存在的行。
  • 丢失更新 : 一个事务的更新操作会被另一个事务的更新操作所覆盖。(可以通过给 SELECT 操作加上排他锁来解决)

8. MVCC

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

基础概念

  • 版本号

系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。

事务版本号:事务开始时的系统版本号。

  • 隐藏的列

MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:

创建版本号:指示创建一个数据行的快照时的系统版本号;

删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。

  • Undo 日志

image.png
MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。

实现过程

当开始一个事务时,该事务的版本号肯定大于当前所有数据行快照的创建版本号,理解这一点很关键。数据行快照的创建版本号是创建数据行快照时的系统版本号,系统版本号随着创建事务而递增,因此新创建一个事务时,这个事务的系统版本号比之前的系统版本号都大,也就是比所有数据行快照的创建版本号都大。(只针对可重复读隔离级别)

  • SELECT
    多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。

把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于等于 T 的版本号,因为如果大于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T 所要读取的数据行快照的删除版本号必须是未定义或者大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。

  • INSERT
    将当前系统版本号作为数据行快照的创建版本号。
  • DELETE
    将当前系统版本号作为数据行快照的删除版本号。
  • UPDATE
    将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。

快照读与当前读

在可重复读级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。

对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:

  • 快照读
    MVCC 的 SELECT 操作是快照中的数据,不需要进行加锁操作。
  • 当前读
    MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。

在进行 SELECT 操作时,可以强制指定进行加锁操作

  • select * from table where ? lock in share mode;需要加 S 锁
  • select * from table where ? for update;需要加 X 锁

9. 分库分表的数据切分

水平切分

水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。

当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
image.png

垂直切分

垂直切分是将一张表按列分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直气氛将经常被使用的列喝不经常被使用的列切分到不同的表中。

在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不通的库中,例如将原来电商数据部署库垂直切分称商品数据库、用户数据库等。
image.png

Sharding 策略

  • 哈希取模:hash(key)%N
  • 范围:可以是 ID 范围也可以是时间范围
  • 映射表:使用单独的一个数据库来存储映射关系

Sharding 存在的问题

  • 事务问题
    使用分布式事务来解决,比如 XA 接口
  • 连接
    可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。
  • 唯一性
    使用全局唯一 ID (GUID)

为每个分片指定一个 ID 范围

分布式 ID 生成器(如 Twitter 的 Snowflake 算法)

10. 主从复制

主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库;主数据库一般是实时的业务数据库,从数据库的作用和使用场合一般有几个:一是作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作;二是可在从数据库作备份、数据统计等工作,这样不影响主数据库的性能。

image.png
主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。

  • binlog 线程 :负责将主服务器上的数据更改写入二进制日志(Binary log)中。
  • I/O 线程 :负责从主服务器上读取- 二进制日志,并写入从服务器的中继日志(Relay log)。
  • SQL 线程 :负责读取中继日志,解析出主服务器已经执行的数据更改并在从服务器中重放(Replay)。

读写分离

主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
image.png
读写分离能提高性能的原因在于:

  • 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
  • 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销;
  • 增加冗余,提高可用性。

11. 索引条件下推ICP特性

MySQL 5.6开始支持ICP(Index Condition Pushdown),不支持ICP之前,当进行索引查询时,首先根据索引来查找数据,然后再根据where条件来过滤,扫描了大量不必要的数据,增加了数据库IO操作。

在支持ICP后,MySQL在取出索引数据的同时,判断是否可以进行where条件过滤,将where的部分过滤操作放在存储引擎层提前过滤掉不必要的数据,减少了不必要数据被扫描带来的IO开销。

在某些查询下,可以减少Server层对存储引擎层数据的读取,从而提供数据库的整体性能。

在开启ICP特性后,对于模糊查询where name = 'Lyn' and nickname like '%SK%'可以利用复合索引 (name,nickname) 减少不必要的数据扫描,提升SQL性能。

index_condition_pushdown:索引条件下推默认开启,设置为off关闭ICP特性。

image.png

关闭ICP特性的执行流程

image.png
当索引选择率低以及数据分布不均匀时,可能会导致扫描大量数据行,只返回少量数据的SQL。

开启ICP特性的执行流程

image.png

12. 数据库调优

调优是在执行器执行之前的分析器,优化器阶段完成的

image.png

(1)排除缓存干扰

在MySQL8.0之前数据库是存在缓存的,因为存在缓存,后续相同sql执行很快。所以在分析SQL查询时间的时候,记得加上SQL NoCache去跑SQL,这样跑出来的时间就是真实的查询时间了。

(2)使用Explain分析sql

(3)使用覆盖索引

如果在我们建立的索引上就已经有我们需要的字段,就不需要回表了。覆盖索引可以减少树的搜索次数,显著提升查询性能。

image.png

(4)使用联合索引

以商品表举例,我们需要根据商品名称,去查商品的库存。假设这是一个很高频的查询请求,可以建立一个,名称和库存的联合索引,这样名称查出来就可以看到库存了,不需要查出id之后去回表再查询库存了

image.png

(5)最左匹配原则

(6)开启索引下推优化ICP特性

可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

(7)唯一索引和普通索引的选择

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。

在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作,通过这种方式就能保证这个数据逻辑的正确性。

需要说明的是,虽然名字叫作change buffer,实际上它是可以持久化的数据。也就是说,change buffer在内存中有拷贝,也会被写入到磁盘上。

将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。

除了访问这个数据页会触发merge外,系统有后台线程会定期merge。在数据库正常关闭(shutdown)的过程中,也会执行merge操作。

image.png
显然,如果能够将更新操作先记录在change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用buffer pool的,所以这种方式还能够避免占用内存,提高内存利用率

那么,什么条件下可以使用change buffer呢?

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。

要判断表中是否存在这个数据,而这必须要将数据页读入内存才能判断,如果都已经读入到内存了,那直接更新内存会更快,就没必要使用change buffer了。

因此,唯一索引的更新就不能使用change buffer,实际上也只有普通索引可以使用。

change buffer用的是buffer pool里的内存,因此不能无限增大,change buffer的大小,可以通过参数innodb_change_buffer_max_size来动态设置,这个参数设置为50的时候,表示change buffer的大小最多只能占用buffer pool的50%。

将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一,change buffer因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

change buffer的使用场景

因为merge的时候是真正进行数据更新的时刻,而change buffer的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做merge之前,change buffer记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。

因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时change buffer的使用效果最好,这种业务模型常见的就是账单类、日志类的系统。

反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在change buffer,但之后由于马上要访问这个数据页,会立即触发merge过程。这样随机访问IO的次数不会减少,反而增加了change buffer的维护代价,所以,对于这种业务模式来说,change buffer反而起到了副作用。

(8)前缀索引

例如使用邮箱作为用户名的情况,每个人的邮箱都是不一样的,那我们是不是可以在邮箱上建立索引,但是邮箱这么长,我们怎么去建立索引呢?

此时可以定义字符串的一部分作为索引。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。

使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。

上面说过覆盖索引了,覆盖索引是不需要回表的,但是前缀索引,即使你的联合索引已经包涵了相关信息,他还是会回表,因为他不确定你到底是不是一个完整的信息,就算你是一个完整的邮箱去查询,他还是不知道你是否是完整的,所以他需要回表去判断一下。

如何优化一个很长的字段的索引?

因为存在一个磁盘占用的问题,索引选取的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。所以可以用hash,把字段hash为另外一个字段存起来,每次校验hash就好了,hash的索引也不大。另外只要区分度高,可以采用倒序,或者删减字符串这样的情况去建立我们自己的区分度。

比如邮箱前面的通用前缀基本上是没任何区分度的,可以用substring()函数截取掉前面的,然后建立索引。比如所有人的身份证都是区域开头的,同区域的人很多,这时可以用REVERSE()函数翻转一下,区分度可能就高了。

(9)条件字段函数操作

如果对字段做了函数计算,就用不上索引了,这是MySQL的规定。对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。

比如 select * from tradelog where id + 1 = 10000 就走不上索引,改写为select * from tradelog where id = 9999就可以。

(10)隐式类型转换

select * from t where id = 1;

如果id是字符类型的,1是数字类型的,你用explain会发现走了全表扫描,根本用不上索引,为啥呢?

因为MySQL底层会对你的比较进行转换,相当于加了 CAST( id AS signed int) 这样的一个函数,上面说过函数会导致走不上索引。

(11)隐式字符编码转换

如果两个表的字符集不一样,一个是utf8mb4,一个是utf8,因为utf8mb4是utf8的超集,所以一旦两个字符比较,就会转换为utf8mb4再比较。

转换的过程相当于加了CONVERT(id USING utf8mb4)函数,那又回到上面的问题了,用到函数就用不上索引了。

(12)隐式字符编码转换

redo log大家都知道,也就是我们对数据库操作的日志,他是在内存中的,每次操作一旦写了redo log就会立马返回结果,但是这个redo log总会找个时间去更新到磁盘,这个操作就是flush。

在更新之前,当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。

内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页“。

那什么时候会flush呢?

  • InnoDB的redo log写满了,这时候系统会停止所有更新操作,把checkpoint往前推进,redo log留出空间可以继续写。
  • 系统内存不足,当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。
  • MySQL认为系统“空闲”的时候,只要有机会就刷一点“脏页”。
  • MySQL正常关闭,这时候,MySQL会把内存的脏页都flush到磁盘上,这样下次MySQL启动的时候,就可以直接从磁盘上读数据,启动速度会很快。

image.png

如何把握flush时机?
Innodb刷脏页控制策略,我们每个电脑主机的io能力是不一样的,你要正确地告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页的时候,可以刷多快。

这就要用到innodb_io_capacity这个参数了,它会告诉InnoDB你的磁盘能力,这个值建议设置成磁盘的IOPS,磁盘的IOPS可以通过fio这个工具来测试。

正确地设置innodb_io_capacity参数,可以有效的解决这个问题。

这中间有个有意思的点,刷脏页的时候,旁边如果也是脏页,会一起刷掉的,并且如果周围还有脏页,这个连带责任制会一直蔓延,这种情况其实在机械硬盘时代比较好,一次IO就解决了所有问题,但是现在都是固态硬盘了,innodb_flush_neighbors=0这个参数可以不产生连带制,在MySQL 8.0中,innodb_flush_neighbors参数的默认值已经是0了。

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL80+完美卸载|小册免费学

发表于 2021-04-15

大家好!俗话说得好“工欲善其事,必先利其器”。想要学习MySQL,当然是实践+理论双双同步,为了重新走一遍当初装MySQL的过程,需要先删MySQL,而笔者在卸载的过程中,发现里面大有文章,特此推出这一篇文章,带你完美卸载MySQL8.0+!

卸载的理由

Maybe太久没用了,重拾MySQL却发现密码再也无法找回!此时又找不到别的方式,只能重装,或者是版本不对,亦或是像我一样要重新装MySQL,不得不给自己的电脑还原一个崭新空荡的环境,结果你发现简单删除之后,再重下,发现各种报错!那是你的卸载姿势不对,需要彻底卸载~

卸载步骤

1、关闭MySQL服务器

方法一:在管理员终端命令行输入net stop mysql

mysql.png

方法二:进入服务->关闭MySQL服务。使用快捷键Win+r 输入services.msc进入服务界面,再右键关闭MySQL服务,或者如第4步,点击停止服务。

服务.png

2、进入控制面板“程序和功能”卸载

因为笔者使用zip文件安装MySQL的,所以不需要执行这一步。

3、删除MySQL安装目录下的MySQL文件夹

笔者的安装目录是C:\other\develop\MySQL,大家根据自己的实际情况进行该操作哈!

软件.png

4、进入注册表编辑器,删除注册表

按下快捷键win+R,输入regedit,回车(或点击确定),打开注册表编辑器。

  • 删除 HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\MySQL 文件夹
  • 删除 HKEY_LOCAL_MACHINE\SYSTEM\ControlSet002\Services\Eventlog\Application\MySQL 文件夹。
  • 删除 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application\MySQL文件夹。

如果没有相应的文件夹,就不用删除了。

PS:由于不同电脑上形式不同,建议利用查找MySQL的方式进行一个个删除)。

5、 删除目录“C:\ProgramData\MySQL”文件夹

删除目录C:\ProgramData\MySQL文件夹,如果显示正在被其他应用所使用,无法进行删除,那么就需要重新启动电脑,就可以删除了。

6、环境变量下“系统变量”路径删除

删除MySQL的系统变量,删除之后点击确认,都要点确认才能生效的!

系统变量.png

7、最后可能MySQL服务还存在:

有时候按照所有的步骤完成了卸载后,服务中却还有MySQL的相关服务存在,为此我们应该删除相关的服务。具体的做法就是:以管理员权限的方式打开cmd命令窗口,然后将在cmd命令中输入命令:sc delete mysql ,通过该命令就可以删除相关的服务。

MySQL8.0+到此完美写在啦,接下来开始愉快的安装之旅吧!

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

tomcat的介绍与安装

发表于 2021-04-15

前言

Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。对于一个初学者来说,可以这样认为,当在一台机器上配置好Apache 服务器,可利用它响应HTML(标准通用标记语言下的一个应用)页面的访问请求。实际上Tomcat是Apache 服务器的扩展,但运行时它是独立运行的,所以当你运行tomcat 时,它实际上作为一个与Apache 独立的进程单独运行的。 诀窍是,当配置正确时,Apache 为HTML页面服务,而Tomcat 实际上运行JSP 页面和Servlet。另外,Tomcat和IIS等Web服务器一样,具有处理HTML页面的功能,另外它还是一个Servlet和JSP容器,独立的Servlet容器是Tomcat的默认模式。不过,Tomcat处理静态HTML的能力不如Apache服务器。

Tomcat简单的说就是一个运行JAVA的网络服务器,底层是Socket的一个程序,它也是JSP和Serlvet的一个容器。

一、基本环境

1、程序架构

1
2
3
4
5
6
scss复制代码C/S(client/server):
优点:部分代码写在客户端,用户体验较好
缺点:服务器更新,客户端也要更新,占用资源大
B/S(browser/server):
优点:客户端只要有浏览器就行了,占用资源小
缺点:用户体验相对而言,体验没那么好

2、服务器:一台电脑,配置比一般的要好

3、前提:确保本机已经安装好JDK。(java–version)查看jdk版本,(7.0 or above,现使用的JDK版本是jdk1.8.0_162)。

3、web服务器软件:处理客户端的请求,返回资源|信息,客户端在浏览器的地址栏中输入地址,然后web服务器,接收请求,然后响应消息

4、web应用:需要服务器支撑,常用web服务器:tomcat(apache),webLogic(oracle),webphere(IBM),iis(微软)

二、安装

对于Windows操作系统,tomcat提供了两种安装文件:exe格式和zip格式。

①exe是可运行的安装程序,只需要双击这个文件,即可开始安装Tomcat。在安装过程中,安装程序会自动搜寻JDK和JRE的位置,并把Tomcat服务加入到Windows操作系统的服务中,同时在“开始”→“程序”菜单中加入Tomcat服务器管理菜单(安装操作简单,但不适合本版升级)。

②zip是一个压缩包,只需要把它解压到本地硬盘即可,这种方式既适合Windows系统下的安装,也适用于其他操作系统(Linux系统)(推荐使用,可以安装多个版本)。

以下演示zip安装:

1、进入官网下载安装程序: tomcat.apache.org/download-90…

2、下载完成后,一直无脑下一步

3、验证安装成功

打开浏览器,在地址栏输入http://localhost:8080 或 http://127.0.0.1:8080进行测试

表示安装成功!

三、tomcat目录介绍

打开tomcat的解压之后的目录可以看到如下的目录结构:

1
2
3
4
5
6
7
8
9
10
makefile复制代码bin:主要是用来存放该目录下存放二进制的可执行文件。这些文件主要有两大类,一类是以.sh结尾的(linux命令),	  另一类是以.bat结尾的(windows命令)。并且很多环境变量的设置都在此处,例如jdk路径、tomcat路径等。
conf:主要是用来存放tomcat的一些配置文件。
lib:主要用来存放tomcat运行需要加载的jar包
logs:主要用来存放tomcat在运行过程中产生的日志文件,非常重要的是在控制台输出的日志。(清空不会对tomcat运 行带来影响)
在windows环境中,控制台的输出日志在catalina.xxxx-xx-xx.log文件中
在linux环境中,控制台的输出日志在catalina.out文件中
temp:临时文件
webapps:存放web项目的目录,其中每个文件夹都是一个项目;如果这个文件下已经存在了目录,那么都是tomcat自带的。注意:其中ROOT是一个特殊的项目,在地址栏中没有给出项目目录时,对应的就是ROOT项目。
work:用来存放tomcat在运行时的编译后文件,例如JSP编译后的文件。
清空work目录,然后重启tomcat,可以达到清除缓存的作用。

四、如何把一个项目发布到tomcat中(如何让其他电脑访问到我这台电脑上的资源)

第一种方式:

1
go复制代码

在webapps下面:
拷贝文件到webapps下的root底下,在浏览器里面直接访问
在webapps下面新建一个文件夹,要访问的资源放在这里面,直接访问

1
2


第二种方式:

配置虚拟路径:在conf里面的server.xml文件配置虚拟路径 在host标签下面的Context标签配置

1
go复制代码

1、在conf/server.xml找到host元素节点
2、加入虚拟路径配置
3、在浏览器输入访问

1
2


第三种方式:

配置虚拟路径

1、在tomcat/conf/catalina/localhost/文件夹下新建一个xml文件,名字自己定义

2、在这个文件内容里面写入context元素,添加配置

3、在浏览器上访问

五、idea配置tomcat

点击Run—EDit Configurations

点击左侧“+”号,找到Tomcat Server—Local

在Tomcat Server -> Unnamed -> Server -> Application server项目下,点击 Configuration ,找到本地 Tomcat 服务器,再点击 OK按钮。

IntelliJ IDEA配置Tomcat完成。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go+gRPC-Gateway(V2) 微服务实战,小程序登

发表于 2021-04-15

系列

  1. 云原生 API 网关,gRPC-Gateway V2 初探
  2. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇
  3. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第二篇
  4. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务(三):RSA(RS512) 签名 JWT

客户端强类型约束,自动生成 API TS 类型定义

protobufjs

官方文档:protobufjs

安装:

1
sh复制代码yarn add protobufjs

node_modules/.bin 会多出如下命令:

  • pbjs
  • pbts

根据 auth.proto 生成 API TS 类型定义

1
2
3
4
5
6
7
8
9
10
sh复制代码PROTO_PATH=../microsvcs/auth/api
PBTS_BIN_DIR=./node_modules/.bin
PBTS_OUT_DIR=./miniprogram/service/proto_gen/auth
mkdir -p $PBTS_OUT_DIR

$PBTS_BIN_DIR/pbjs -t static -w es6 $PROTO_PATH/auth.proto --no-create --no-encode --no-decode --no-verify --no-delimited -o $PBTS_OUT_DIR/auth_pb_tmp.js
echo 'import * as $protobuf from "protobufjs";\n' > $PBTS_OUT_DIR/auth_pb.js
cat $PBTS_OUT_DIR/auth_pb_tmp.js >> $PBTS_OUT_DIR/auth_pb.js
rm $PBTS_OUT_DIR/auth_pb_tmp.js
$PBTS_BIN_DIR/pbts -o $PBTS_OUT_DIR/auth_pb.d.ts $PBTS_OUT_DIR/auth_pb.js

脚本已被放置在 miniprogram/gen_ts.sh,在 miniprogram 目录执行 sh gen_ts.sh 即可生成如下文件:

  • miniprogram/miniprogram/service/proto_gen/auth/auth_pb.js
  • miniprogram/miniprogram/service/proto_gen/auth/auth_pb.d.ts

修改 app.ts

引入:

1
ts复制代码import { auth } from "./service/proto_gen/auth/auth_pb"

在文件里面做如下改动:

1.png

从上图可以看到有属性提示。这里我们也加入了一个 camelcase-keys 包。它主要用来将属性 key 从网络上传输的 expires_in 转换为 expiresIn。

Token 验证

编码实战

具体代码位于:microsvcs/shared/auth/token/token.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码type JWTTokenVerifier struct {
PublicKey *rsa.PublicKey
}
func (v *JWTTokenVerifier) Verify(token string) (string, error) {
t, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(t *jwt.Token) (interface{}, error) {
return v.PublicKey, nil
})
if err != nil {
return "", fmt.Errorf("cannot parse token: %v", err)
}
if !t.Valid {
return "", fmt.Errorf("token not valid")
}
clm, ok := t.Claims.(*jwt.StandardClaims)
if !ok {
return "", fmt.Errorf("token claim is not StandardClaims")
}
if err := clm.Valid(); err != nil {
return "", fmt.Errorf("claim not valid: %v", err)
}
return clm.Subject, nil
}

测试用例

  • 正常
  • token 过期
  • 坏的 token
  • 签名错误

具体代码位于:microsvcs/shared/auth/token/token_test.go

Refs

  • API Security : API key is dead..Long live Distributed Token by value
  • Demo: go-grpc-gateway-v2-microservice
  • gRPC-Gateway
  • gRPC-Gateway Docs
1
2
3
4
sh复制代码我是为少
微信:uuhells123
公众号:黑客下午茶
加我微信(互相学习交流),关注公众号(获取更多学习资料~)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

VirtualBox安装VBoxGuestAdditions

发表于 2021-04-15

VirtualBox安装VBoxGuestAdditions实现共享文件夹等功能

  1. 在宿主机上挂载VBoxGuestAdditions.iso
  2. 在虚拟机上更新内核版本及安装编译依赖
1
2
3
4
go复制代码yum install -y kernel-devel gcc
yum -y upgrade kernel kernel-devel
yum install -y gcc-c++ automake autoconf libtool make elfutils-libelf-devel
yum install bzip2

综合命令

1
go复制代码sudo yum install -y kernel-devel gcc upgrade kernel kernel-devel gcc-c++ automake autoconf libtool make elfutils-libelf-devel bzip2
  1. 查看内核版本,移除旧的内核版本
1
2
3
arduino复制代码uname -r
rpm -qa | grep kernel-[0-9]
yum remove kernel-xxxxx
  1. 挂载镜像文件

vagrant软件挂载后命令

1
2
3
4
5
bash复制代码ll /dev/src0
mkdir -p /media/cdrom
mount /dev/sr0 /media/cdrom
cd /media/cdrom
./VBoxLinuxAdditions.run
  1. 直接文件挂载命令
1
2
3
4
5
6
7
bash复制代码    wget http://download.virtualbox.org/virtualbox/4.3.8/VBoxGuestAdditions_4.3.8.iso
sudo mkdir /media/VBoxGuestAdditions
sudo mount -o loop,ro VBoxGuestAdditions_4.3.8.iso /media/VBoxGuestAdditions
sudo sh /media/VBoxGuestAdditions/VBoxLinuxAdditions.run
rm VBoxGuestAdditions_4.3.8.iso
sudo umount /media/VBoxGuestAdditions
sudo rmdir /media/VBoxGuestAdditions

5、手动挂载(可以选择自动挂载,在设置分享文件时)
mount -t vbocsf shared /mnt/shared

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

避免掉进“重造轮子”的坑 从审核系统说起

发表于 2021-04-15

作者:闲鱼技术——莫癫

前言

在研发团队发展到一定规模,同一领域问题不可避免地会存在多种解决方案。典型的,不同业务线会开发和使用不同的测试框架,很多业务线会重新开发特征中心、配置中心、规则引擎和投放平台。不可否认有业务特殊性或者已有方案无法满足等原因导致合理建设,其中有重造轮子的现象。

作者在半年前开始投身闲鱼会玩社区治理,从用什么方案、是不是会重造轮子的自我怀疑,到后面沉淀会玩社区的通用审核系统高效应对运营需求的豁然开朗,这段经历颇有收益。本文通过还原这段经历和其中的思考,谈谈在解决相同领域的问题如何避免陷入重造轮子的泥潭,达到高效解决业务问题实现最大技术价值的目标。Hub_(PSF).png

不要重复自己

软件工程中一个基本原则:DRY(不要重复自己),也就是强调抽象和复用,这是避免重造轮子最基础的要求。运用算法、设计模型和框架设计思想,能从不同角度和不同层次避免重复自己,而长期驻扎业务线的同学还需要对业务抽象,从解决一个又一个的业务问题,转变成解决一类又一类问题的工作方式解放生产力。

不像平台型产品经理在前期就会描绘好产品的完整蓝图,业务线产品经理则更多时候是将用户侧的产品形态或者运营的单点工具诉求翻译成源源不断的需求文档,前后需求之间可能有关联也可能没关联。在时间和空间不连续的需求中,识别出通用流程和可复用能力,提前抽象、规划和设计就变得极其重要而有难度。在闲鱼会玩社区业务起步阶段我们对社区治理方面的需求进行了充分调研和提炼:

•紧跟业务目标和政策法规:会玩社区定位为一个纯净、有调性和有氛围的社区,需要运营全面洞察和协作把控社区的人、内容和场。同时网信办加大网络信息整治和处罚力度,对自查自纠的完整全面和及时性提出了更高的需求;•调研行业和竞对解决方案:闲鱼会玩是一个同时承载UGC和PGC内容的社区,同所有同类社区一样,内容、话题和创作者等社区各维度元素在安全防控、分类和原创保护等都有着刚性的审核或标注诉求;•与合作方充分交流:团队内外不乏在业务和技术上沉淀深厚的前辈,这是个吸收别人经验和验证自身想法的很好契机;

这些方式帮忙高效定位业务现处阶段和中长期在同领域的诉求,结合当前需求,可以合理看到需要支撑风险决策、治理和打标等审核或类审核需求,具备业务接入、岗位培训、审核、质检和不断优化效能的通用流程,不同的业务关键指标的诉求也基本一致,只是对人效和实时性的敏感程度不同而已。可以沉淀通用且支持扩展的审核系统承载上述业务,避免自我复制轮子快速接入业务。image.png审核业务抽象

不要重复别人

另一种更为常见的重造轮子就是重复别人。澳大利亚的JohnKeogh于2001年申请注册“圆形的交通设施”(轮子)为专利,于当年被评为2001年的搞笑诺贝尔奖科技奖。开源社区以及企业内部的软件轮子频出,不乏几种原因:

1.闲暇时间个人学习或兴趣驱动2.不知道已有可用解决方案3.别人东西体验太差4.已有方案不能完全满足需求,无法适配、扩展,或成本过高5.已有实现无人维护或者维护成本过高6.战略原因对战略方向用赛马方式进行内部竞争7.组织架构原因导致无法合作或者合作效率低8.纯KPI原因

在以效益为主要目标的生产企业内部,其中3、4、5、6、7是比较合理的内在诉求。而第4点“已有方案能多大程度满足诉求”实际上决定这个方案是否能解决同类问题,这种情况的存在导致重造轮子的界定会存在一定程度的模糊。所以往往会从多个角度评估重建的合理性和必要性。

在抽象好并确定需要一个通用审核系统后,抉择复用其他团建的基建能力还是从零开始建设,需要花大量精力去调研和评估。在集团内部有迹可循的成熟审核系统有多个。每个审核系统都有清晰的定位,解决垂直领域的问题:

•系统A:集团最为普及的工作流系统。有优秀的流程编排能力,但是不支持复杂页面布局、多态审核结果和嵌入各类交互组件,同步不支持处理人效和质量的优化;

•系统B:封闭的内容审核平台,只支持接入该生态的帖子审核;

•系统C:安全中心的审核系统,定位如其名,主要针对内容安全进行抓黑放白;

•系统D:用于知识标注,不具备审核流的协作能力;

可以看到每个系统都能满足业务的部分诉求,却无法通过迭代升级扩展边界和改变其定位,那么重建是不可避免的。

怎么设计和演进是可承受的

论证清楚新建系统的必要性,系统建设成本控制在可承受范围内也同样重要。可承受,不仅包括人力和时间资源投入是否团队可以承受,更重要的是迭代周期是否是业务可承受。以系统建设为理由给业务画饼的方式逼迫业务妥协,即使达成目的,却间接导致业务受损,就丢了西瓜拣芝麻。 在落地审核系统的过程,主要通过系统设计和演进策略的反复优化来降本提效,最大程度按时、保质、保量支持业务发展,同时避免系统腐化、提高迭代速度。

系统设计

在规划初期,只要时间允许,可以尽可能花较多时间在中长期系统规划,保持架构合理。架构确定后最理想需要贯穿很长一段时间的版本迭代,坚持一些原则帮忙审核系统架构在未发生大变化的前提仍在正确的方向迭代发展:

•微核心和插件化设计:保持核心组件层足够简洁,支持核心流程和搭建基础的插件容器。扩展功能进行插件实现;•借鉴和遵循成熟模式:剖析和借鉴同类系统经验,成熟的系统沉淀了成熟的方法论,甚至如集团BPMS实现很大程度是工业标准BPMS的一个典型成功案件,XAP是对业内内容审核流程的极大总结和实践,在理论和功能完善性都有很大都体现;•对可扩展和灵活性保持克制:不为不确定的灵活扩展做过度设计,如BPMS使用的流程引擎灵活强大且支持可视化的编排能力,在社区审核仅需线性和有限节点的审核流中则显得鸡肋,反而大大增加系统内核的复杂程度;•重点突破同类系统缺陷:某系统配置不统一、送审强依赖定时圈选无法保证流程实时等,这些已知问题在初期得到重视是可以很好得到解决的;•能力融合:前面提到的垂类审核系统已经在支持闲鱼会玩社区的部分审核业务,通过将其抽象为流程的节点类型之一进行对接,新建系统专注于目前短板能力建设,极大节省能力补全的投入;

image.png社区审核系统

社区审核系统在经历三个版本的迭代后,目前整体架构并未发生变更和重构,只是对各模块的插件进行补充以支持更多业务场景。

MVP和演进策略

MVP(最简化可实行产品)只需要完成主链路的流转,从数据接入、工单流转并完成简单派单和人工审核功能,这已经可以满足简单标注和初期的业务审核需求,每个模块也只实现百分百必要的能力:

•接入侧规约数据协议规范,支持消息输入输出,具备较好的容错性;•基本完备的流程流转内核:即上述抽象的微内核,需要在一期基本完成并保证在后期不会发生较大变化;•派单只需要实现拉单模式,且不需要支持规则化分派;•根据业务诉求,实现最简化的定制化审核工作台;

image.png社区审核系统MVP版本

演进策略跟MVP遵循的原则无差,都是围绕每期需要支撑的业务,并抽象为规划图的具体位置逐步迭代填充,以完成大图的完整拼图。 审核系统的MVP版本在两周完成开发上线,快速支撑了闲鱼社区圈子的审核,并在后续的迭代中完成安全中心的对接,避免业务侧直接对接安全中心长达一个月的排期和等待更长时间实施上线。后续基本保持每个版本两周的迭代周期,快速支持了后续的需求。

避免被重复

在日常工作中避免因为各种原因重造轮子的同时,也有义务尽可能地去避免内部在相同领域出现重造轮子的行为。抛开为了造轮子而造轮子的行为之外,很多轮子的产生原因更多是客观因素导致,在一定程度上是可以尽可能去规避: • 合理的架构和实现:相信很多大部分系统设计初衷都是为了解决一类问题,而不是解决一个问题。往往会因为各种原因达不到理想的状态,需要花足够多的经历进行前期设计,保持内核的开闭、插件功能的单一职责等,遵循良好的设计模式,并定期回顾优化; • 打破信息烟囱: 在合适的阶段进行总结和思考,用分享或文章的方式传播给大家,与外界发生互动。也就尽可能避免了其他团队不知道轮子的存在而选择另起炉灶; • 避免烟囱式系统:不具备足够的开放能力,除了完成已有的能力和支撑已有业务,不再探索可能的业务融合,逐渐会成为烟囱系统。很多时候具备保持大门敞开这样的开放性并不够,还需要主动挖掘需求,用各种方式降级业务接入门槛,接入更多业务,达到业务支撑和迭代的良性循环;

举两个例子。一是审核在社区内部业务的支持上,一开始只支持RPC和消息按标准消息格式接入方式,这对业务方有一定的理解和接入成本。在数据对接上实现插件化的数据适配方案,并实现核心业务领域的搜索数据源对接完成插件沉淀,并针对常见主体类型搭建了可插拔组件的审核页面,各域在需求对接只需要进行数据的圈选,免去复杂数据格格式的对接;另外,针对用户认证业务在展现形式和标注能力上适配异构的信息,并对公用的数据源进行对接沉淀为复用组件,不同认证业务关注于对接差异的数据即可。

总结

重复是枯燥无聊的,避免重复和被重复是每个开发者解放生产力和成长的必经阶段。从业务、功能和流程各个维度抽象,并充分调研和论证新建系统的必要性,在前期的设计上做好通用性、可扩展性和功能的平衡,以业务诉求可满足、资源投入和业务节奏可承受为准则制定MVP版本和迭代版本。同时提高系统的开放性,主动拥抱业务和发挥技术最大价值。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…685686687…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%