2023年4月

1.前言

同样另外一个非常有意思的题目,值得我们思考。大概背景是这个样子的。如果有一个事务A进行插入 id > 100, 同时另外一个事务B进行更新update id > 100。那么事务B是否会更新成功。我们来画一个时序图:

time 事务A 事务B 备注
T1 insert id > 100 set status = 1
T2 update id > 100 set status = 2
T3 最后id > 100 status是为1 还是为2呢

2.代码

我们从事务的四个隔离性来分别讨论这个问题。所有代码如下,仅仅是隔离性级别修改。修改是status状态。
以下所有操作类似于打开两个浏览器,首先请求事务A,事务A执行过程中,在请求事务B,观察结果。

  1. 事务A 进行插入
@RequestMapping("/test/publish/submit")
public String testPublish1() {
	log.info("start...");
	transactionTemplate.execute(new TransactionCallback<String>() {
		@Override
		public String doInTransaction(TransactionStatus status)  {
			for (long i = 1000; i < 3000; i++) {
				TElement element = new TElement();
				element.setfElementId(i);
				element.setfElementName("666666");
				element.setfElementStatus((byte) 1);
				mapper.insertSelective(element);
			}
			return "OK";
		}
	});
	log.info("end...");
	return "ok";
}
  1. 事务B 进行更新
@RequestMapping("/test/publish/submit2")
public String testPublish2() {
	log.info("start...");
	transactionTemplate.execute(new TransactionCallback<String>() {
		@Override
		public String doInTransaction(TransactionStatus status)  {
			// @Update({"UPDATE t_element SET f_element_status=#{status} WHERE f_element_id > #{elementId}"})
			mapper.update(1000L, (byte) 2);
			return "OK";
		}
	});
	log.info("end...");

	return "ok";
}

2.1读未提交(READ UNCOMMITTED)级别

经过以上步骤测试,我们得出了如下日志

[INFO  2023-04-13 11:39:29.941] [http-nio-8099-exec-2] [df9c9572-a906-4f5c-91a2-c1fb4a87adcf] - [TransactionInsertUpdateTest.java.testPublishInsert:36] [start...]

[INFO  2023-04-13 11:39:31.708] [http-nio-8099-exec-3] [5651390e-f3be-4d35-81dc-cc7bb0062f40] - [TransactionInsertUpdateTest.java.testPublishUpdate:57] [start...]
[INFO  2023-04-13 11:39:31.760] [http-nio-8099-exec-3] [5651390e-f3be-4d35-81dc-cc7bb0062f40] - [TransactionInsertUpdateTest.java.testPublishUpdate:66] [end...]

[INFO  2023-04-13 11:39:42.952] [http-nio-8099-exec-2] [df9c9572-a906-4f5c-91a2-c1fb4a87adcf] - [TransactionInsertUpdateTest.java.testPublishInsert:50] [end...]

我们可以看到事务A在29秒开始执行,并且在42秒执行完成。
事务B在31秒开始执行,
并且没有阻塞
,立刻执行完成。
我们再来观察以下数据库中的数据,我们发觉status=1,也就是事务B没有更新成功
因此在READ UNCOMMITTED下,事务会有问题。
image

2.2读已提交(READ COMMITTED)

经过以上步骤测试,我们得出了如下日志。我们可以看到在RC级别下,出现了和READ UNCOMMITTED同样的现象。
具体原因如上,就是没有加锁。

[INFO  2023-04-13 11:46:59.684] [http-nio-8099-exec-3] [a11f29f2-0658-42b4-8eba-28b5f94f9037] - [TransactionInsertUpdateTest.java.testPublishInsert:36] [start...]

[INFO  2023-04-13 11:47:01.029] [http-nio-8099-exec-2] [a9d04f6e-4efe-4ed7-a2df-c2454d8d7946] - [TransactionInsertUpdateTest.java.testPublishUpdate:57] [start...]
[INFO  2023-04-13 11:47:01.090] [http-nio-8099-exec-2] [a9d04f6e-4efe-4ed7-a2df-c2454d8d7946] - [TransactionInsertUpdateTest.java.testPublishUpdate:66] [end...]

[INFO  2023-04-13 11:47:12.574] [http-nio-8099-exec-3] [a11f29f2-0658-42b4-8eba-28b5f94f9037] - [TransactionInsertUpdateTest.java.testPublishInsert:50] [end...]

2.3可重复读(REPEATABLE READ)

经过以上步骤测试,我们得出了如下日志。注意以下
testPublishUpdate
日志和以上日志的不同。

[INFO  2023-04-13 11:50:47.428] [http-nio-8099-exec-3] [83cac49b-f44e-44bd-a8ef-9d60714016f6] - [TransactionInsertUpdateTest.java.testPublishInsert:36] [start...]
[INFO  2023-04-13 11:50:48.851] [http-nio-8099-exec-2] [f66d62af-aa3f-4d3e-9f66-c97307d6e38e] - [TransactionInsertUpdateTest.java.testPublishUpdate:57] [start...]


[INFO  2023-04-13 11:50:53.872] [http-nio-8099-exec-3] [83cac49b-f44e-44bd-a8ef-9d60714016f6] - [TransactionInsertUpdateTest.java.testPublishInsert:50] [end...]
[INFO  2023-04-13 11:50:53.895] [http-nio-8099-exec-2] [f66d62af-aa3f-4d3e-9f66-c97307d6e38e] - [TransactionInsertUpdateTest.java.testPublishUpdate:66] [end...]

在事务A insert的时候,事务B update的时候阻塞了。这时候其实是
间隙锁
发挥了作用,也就是必须等事务A执行完毕之后,事务B才会获取锁,去update更新。那这时候就会更新成功了。数据库数据如下:

image

2.4序列化

隔离级别最严格的级别。同2.3会阻塞然后更新成功。

3.结论

在隔离级别为
读未提交(READ UNCOMMITTED)
以及
读已提交(READ COMMITTED)
情况下,会出现事务更新失败的情况。
本质上就是没有加锁导致的,而在RR级别,给事务A加上了间隙锁,事务B必须等待才能update成功。是用了锁的的方式来解决的,但可能存在效率的问题。所以锁尽量细化,比如行锁 > 间隙锁 > 记录锁 > 表锁。都是平衡效率以及安全的一种手段。

摘要:
多跳查询能力也是一个衡量产品性能非常重要的指标。

本文分享自华为云社区《
聊聊超级快的图上多跳过滤查询
》,作者:弓乙。

在图数据库/图计算领域,多跳查询是一个非常常用的查询,通常来说以下类型的查询都可以算作是多跳过滤查询:

1.查询某个用户的朋友认识的朋友 --二跳指定点label的查询2.查询某个公司的上下游对外投资关系 --N跳指定方向过滤查询3.查询某个公司实际持股股东 --N跳内带过滤查询4.搜索可提供某个零部件的供货商 --N跳内带过滤的until查询5.局点变更影响分析 --N跳内带过滤查询

如下图,可用3跳查询得到网讯公司A所有的对外投资机构。

与此同时,多跳查询能力也是一个衡量产品性能非常重要的指标,比如LDBC(Linked Data Benchmark Council)的交互式查询场景下就设计了多个考察图数据库系统多跳查询能力的测试用例,交互式查询Interactive的Complex Query中有多个用例均为多跳查询,如下图是一个查朋友最近发送的消息的IC2用例,是一个经典的图上2-hop查询。

在图计算的尺度里,多跳过滤某些情况下被称为k-hop算法,BFS,DFS算法,区别仅在于traversal的策略是深度优先还是广度优先。

而在图数据库中一般将多跳过滤看做是
需要特殊优化
的模式匹配查询(cypher)或可组合的复合查询(gremlin)。

以下展示常用的图查询语言gremlin/cypher的二跳查询的写法,结果均为返回李雷朋友的朋友:

gremlin: g.V('李雷').out('朋友').out('朋友')

cypher: match (n)-->(m1:朋友)-->(s1)-->(m2:朋友)-->(s2) where id(n)='李雷' return s2 

这两个语句
轻盈又直观
,看起来一切都被解决地如此优雅。

但事实真的如此吗?

很遗憾,当我们兴致勃勃地构图,将数据导入图数据,再使用类似上述语句查询实际业务场景时,你也许会惊讶地发现,也许结果与我们所期待的并不一致。

接下来,我将展开说说为什么使用多跳过滤查询会比我们想象中的更复杂,了解了图上遍历的概念后,我们能把而基于多跳过滤这一特性,我们又能怎么做使得这个重要的查询又快又流程呢?

功能那些事儿

上面我们介绍了查询一个用户朋友的朋友的例子,这里我们假设业务场景是向李雷推荐好友,思路是:向他推荐其好友的好友,但是
推荐的好友中不应包含李雷本身的好友
,比如图中小明同时是李雷的一跳好友和二跳好友。这时我们不应向李雷推荐小明,因为她已经是李雷的好友了。

仅仅增加了一个返回的二跳邻居不包含一跳邻居这个条件,让我们来看下与上面单纯的2跳查询的gremlin和cypher变成什么样了?

gremlin: 
g.V(
"李雷").repeat(out("朋友").simplePath().where(without('1hop')).store('1hop')).
times(
2)

cypher: 
match (a)
-[:朋友]->(d) where id(a)='李雷' with a, collect(d) asneighbor
match (a)
-[:朋友]-(b)-[:friend]-(c)where not (c inneighbor)return c

不知道各位有什么感觉?如果是不熟悉图查询语言的朋友们看到这里一定两眼发黑了吧哈哈。

不用担心,如果我们灵活使用一些特性,也许事情会变得简单,比如这是一个GES原生API多跳过滤查询Path Query的json请求:

{"repeat": [{"operator": "outV"}],"times": 2,"vertices": ["李雷"],"strategy":"ShortestPath"}

假如我们可以把它翻译为gremlin的写法的话,它大概是:

g.V('李雷').repeat(out()).times(2).strategy('ShortestPath')

以上的写法除了strategy('ShortestPath')其他本身就是gremlin已经支持的语法,意为
-查询从李雷出发以ShortestPath为遍历策略的二跳邻居。

遍历策略是什么?

理解遍历策略是了解
多跳过滤
的基石,我们这里从图论里几个定义展开:

A walk is a finite or infinite sequence of edges which joins a sequence of vertices.[2]
Let G
= (V, E, ϕ) be a graph. A finite walk is a sequence of edges (e1, e2, …, en − 1) for which there is a sequence of vertices (v1, v2, …, vn) such that ϕ(ei) = {vi, vi + 1} for i = 1, 2, …, n − 1. (v1, v2, …, vn) is the vertex sequence of the walk. The walk is closed if v1 = vn, and it is open otherwise. An infinite walk is a sequence of edges of the same type described here, but with no first or last vertex, and a semi-infinite walk (or ray) has a first vertex but no last vertex.
A trail
is a walk in which all edges are distinct.[2]
A path
is a trail in which all vertices (and therefore also all edges) are distinct

以上应用wiki中对于图上不同的点集组成的边集说明,详情见Path (graph theory) - Wikipedia。

以下,我将几个相似又不同的概念用四个维度区分开。

以上5个概念均指代在G=(V,E,φ)中,由点V,边E组成的序列。

上图中,对于序列a->c->d->f,我们可以将它称为walk, trail, path,三者都可以。因为该序列的起点a与终点f不同,不属于对序列要求close状态circuit和cycle。

而序列a->c->a->c, 我们只能将其归为walk。因为其不闭合不属于circuit和cycle,且点有重复(a,c两个都有重复)不属于path,边集有重复(a->c的边有重复)不属于trail。

遍历策略对
最终的结果和遍历效率
都有决定性的影响。

这里简单说明下新增的shortestPath策略:

  • Walk:以图论中划定的walk进行图遍历:即在traversal的过程中允许经过重复的点和重复的边。
  • ShortestPath:以shortestPath的规则进行图遍历:即在BFS traversal的过程不会遍历在前面跳数出现过的点。在这种模式下的路径每个终点到起点都是最短路径。

BFS与DFS

影响遍历顺序的另一个角度一般我们分为:

  • BFS - Breadth-first search 广度优先搜索
  • DFS - Depth-first search 深度优先搜索

BFS在图遍历时会优先遍历一个点的所有邻居,再遍历其邻居的邻居,而DFS会优先遍历点的邻居的邻居,直到到达最深的节点。

我们可以设置一个batchSize,表示在进行BFS时不遍历全部的邻居,而是每层以batchSize的数量进行遍历,然后再以batchSize的个数遍历下一层邻居。

可以推断出,当batchSize=1时,该BFS约等于DFS的遍历策略。

性能那些事儿

多跳过滤的性能受很多因素影响。以下总结一些会影响到性能的因素以供参考:

而对于不同规模的图,多跳过滤查询的TPS大部分情况下并不取决于图的点边规模,而是与图的密度密切相关。

比如一个二跳查询,那么TPS就与该图的二跳邻居分布强相关。

简单来讲,多跳过滤最终的性能主要受
遍历过程中触达节点的个数影响
。同样规模的图,其多跳过滤tps可能相差很大。大部分情况下基准测试仅提供横向比较,考验的是图数据库/图引擎本身的性能指标,有一定参考价值,但仍以实际业务图表现为主。

经验上,稀疏的图性能表现大大好于稠密的图。

多跳查询的使用

为了简化多跳过滤查询的表达,我们用一些参数来表达查询过程。如前面章节介绍过的遍历策略,batchSize等。

本章我们将一一介绍以下特性参数:

接下来我们以GES的path query(filterquery version2版本)接口来举例介绍多跳过滤查询的使用。

该接口支持对
多跳过滤查询,循环执行遍历
查询进行加速。可以看做是gremlin的repeat语句的扩充表达,例如以下gremlin语句:

g.V('1','2').repeat(out()).times(2).emit().dedup()

以下为该接口的2跳/3跳查询在1亿规模图上的测试情况。

其中,

  • filterV2 - GES
    的多跳过程查询原生接口,该API性能最佳,TPS与延迟表现优异。
  • Cypher - GES
    的cypher查询(较老版本)。
  • Neo4j - Neo4j
    community 4.2.3版本cypher。
  • gremlin - GES
    的gremlin,未在图中体现,实际性能最差,故未做对比。


请求样例1

POST /ges/v1.0/{projectId}/graphs/{graphName}/action?action_id=path-query
{
"repeat": [{"operator": "outV"}
],
"emit": true,"times": 2,"vertices": ["1","2"]
}

以上请求等价于gremlin语句:

g.V('1','2').repeat(out()).times(2).emit().dedup()####

特性参数简要说明

速查参数表

filterV2的参数过于复杂,需要花一定的时间去理解?请看下面总结出的速查表,帮助您快速使用repeat模式:

PS:strategy策略的说明查看下方


emit模式

emit是一个filtered query参数中对其他各个参数影响最大的参数。我们将其定义为,是否输出query过程中的点,其含义也与gremlin中的emit一致。

下面将介绍,在不同模式下emit的表现形式:

上图中,假定我们从点a出发,执行filtered khop query的查询。

strategy

图遍历过程中使用的策略,目前可选:ShortestPath和Walk。

  • Walk:以图论中划定的walk进行图遍历:即在traversal的过程中允许经过重复的点和重复的边。
  • ShortestPath:以shortestPath的规则进行图遍历:即在BFS traversal的过程不会遍历在前面跳数出现过的点。在这种模式下的路径每个终点到起点都是最短路径。

上图中,假定我们从点a出发,执行query的4跳查询。

Walk: a->c->a->b, a->c->a->c, a->c->d->f, a->c->d->c

ShortestPath: a->c->d->f

简单查询

1. 查询从a出发的第三跳邻居

使用gremlin查询:

g.V('a').out().out().out()
g.V(
'a').repeat(out()).times(3)

以上两种写法是等效的,结果为点f。路径为a->c->d->f。

对应在API参数为:

{"repeat": [
{
"operator": "outV"}
],
"emit": false,"times": 3,"vertices": ["a"]
}

2. 查询从a出发的三跳内邻居

使用gremlin查询:

g.V('a').repeat(out()).times(3).emit()

结果为b,c,d,e,f。

对应在API参数为:

{"repeat": [
{
"operator": "outV"}
],
"emit": true,"times": 3,"vertices": ["a"]
}

组合模式说明

emit+strategy

1. 查询第三跳邻居

在上面的图中,如果按照[简单查询1](#1. 查询从a出发的第三跳邻居), 将得到点f, c, b。

路径为a->c->a->b, a->c->d->f, a->c->d->c。

如果,我们只想得到f, 而不希望取到前两跳已经出现过的点c和b。则需要使用strategy模式中的ShortestPath。ShortestPath模式保证API在traversal的过程中起始点到各点是最短路径。该模式能够有效降低多跳查询中指数增长的查询量。

gremlin的写法为:

g.V("a").repeat(out().simplePath().where(without('hops')).store('hops')).times(3).path()

结果为仅为a->c->d->f。

对应在API参数为:

{"repeat": [
{
"operator": "outV"}
],
"emit": false,"strategy": "ShortestPath","times": 3,"vertices": ["a"]
}

emit+until

1.提前终止traversal模式说明

我们以上面的图来说明该模式,当我们不清楚查询需要经过多少跳,但希望通过某些条件提前终止遍历,可以用到until。

如以下两个问题:

1.得到从a出发N跳内label=book的点。2.得到从a出发N跳内所有点,停止查询的条件为遇到label=book的点。

以上问题可以配合emit参数来解决,用gremlin可以写为:

Q1: g.V('a').repeat(out()).until(hasLabel('book'))
Q2: g.V(
'a').repeat(out()).until(hasLabel('book')).emit()

与之对应的API为:

{"repeat": [
{
"operator": "outV"}
],
"until": [
{
"vertex_filter": {"property_filter": {"leftvalue": {
“label_name
":""},"predicate": "=","rightvalue": {"value": "book"}
}
}
}
],
"emit": false/true,//这里根据Q1,Q2的情况选择emit的值。 "times": 5,"vertices": ["a"],"strategy": "Walk"}

repeat模式说明

repeat+times

可通过参数repeat+times实现多种形式的多跳过滤及repeat模式过滤。

1.仅多跳过滤

gremlin的写法为:

g.V("a").repeat(out().in()).times()

g.V(
"a").out().in()

对应在API参数为:

{"repeat": [
{
"operator": "outV"},
{
"operator": "inV"}
],
"strategy": "Walk","times": 2,"vertices": ["a"]
}

2.repeat mode

假如我们从点a出发,查询其带方向的四跳邻居。即:

gremlin的写法为:

g.V('a').repeat(out('user').out().has('age',18)).times(2)

g.V(
'a').out('user').out().has('age',18).out('user').out().has('age',18)

对应在API参数为:

{"repeat": [
{
"operator": "outV","edge_filter": {"property_filter": {"leftvalue": {"label_name": ""},"predicate": "=","rightvalue": {"value": "user"}
}
}
},
{
"operator": "outV","vertex_filter": {"property_filter": {"property_name": {"label_name": "age"},"predicate": "=","rightvalue": {"value": "18"}
}
}
}
],
"times": 4,"emit": false,"vertices": ["a"]
}

by模式说明

by模式当前支持两种形式:

  • select+by mode
  • by mode

by mode

该模式可针对输出的点进行输出格式上的过滤,返回的格式形如:

{"data": {"vertices": [
{
"id": "47","label": "user"},
{
"id": "51","label": "user"}
]
}
}

例如针对二跳邻居,我们可以通过参数by对id,label,property进行过滤:

g.V("a").repeat(out()).times(2).by(id())

转化为filtered query V2的形式为:

{"repeat": [
{
"operator": "outV"}
],
"times": 2,"vertices": ["a"],"by": [{"id": true}]
}

select + by mode

该模式可任意选择traverse路径上经过的N层。但每层只能在by中指定一个输出,返回的格式形如:

{"select": [
[
"李雷", "小明","小智"],
[
"李雷","韩梅梅", "小智"],
[
"李雷", "韩梅梅", "小霞"]
]
}

下面我们来介绍一下select+by模式。如下图,我们希望返回李雷的二跳邻居的路径情况。

{"repeat": [
{
"operator": "outV"}
],
"times": 2,"vertices": ["李雷"],"by": [{"id": true},{"id": true},{"id": true}],"select": ["v0", "v1", "v2"]
}

g.V('1').as('v0').both().as('v1').both().as('v2').select('v0','v1','v2').by(id()).select(values)

在上面body体中,我们使用select自带的隐含层数别名v0, v1, v2。其分别表示
用户输入的点集第0层-v0, K跳中的第1层-v1, K跳中的第2层-v2。

select中选中的别名与by中指定的格式一一对应。

以上案例输出格式形如:

{"select": [
[
"李雷", "小明","小智"],
[
"李雷","韩梅梅", "小智"],
[
"李雷", "韩梅梅", "小霞"]
]
}

当然,我们也可以有更多的更丰富的输出。比如我们希望将输入点李雷的id和label都展示在路径中,并且去掉了中间第一跳的好友小明和韩梅梅,直接显示李雷及其第二跳好友的关系:

{"repeat": [
{
"operator": "outV"}
],
"times": 2,"vertices": ["李雷"],"by": [{"id": true},{"label": true},{"id": true}],"select": ["v0", "v0", "v2"]
}

最终展示结果是对路径自动去重的。比如在这个例子中,李雷到小智有两条路径,中间分别经过好友小明和韩梅梅, 但最终仅显示了一条。

以上案例输出格式形如:

{"select": [
[
"李雷", "person", "小智"],
[
"李雷", "person", "小霞"]
]
}

案例

案例一.好友推荐

我们向李雷推荐好友,思路是:向他推荐其好友的好友,但是推荐的好友中不应包含李雷本身的好友,比如图中韩梅梅同时是李雷的一跳好友和二跳好友。这时我们不应向李雷推荐韩梅梅,因为她已经是李雷的好友了。

下面将分别展示使用gremlin,cypher和下一代filter query查询,返回均为推荐路径:李雷->李雷好友->推荐好友。

gremlin

gremlin>g.V("李雷").repeat(out("friend").simplePath().where(without('1hop')).store('1hop')).
times(
2).path().by("name").limit(100)
gremlin
>[李雷,小明,小智]
[李雷,韩梅梅,小智]
[李雷,韩梅梅,小霞]

cypher

match (a)-[:friend]->(d) where id(a)='李雷' with a, collect(d) asneighbor
match (a)
-[:friend]-(b)-[:friend]-(c)where not (c inneighbor)returna.name, b.name, c.name
[
{
"row": ["李雷", "小明","小智"],"meta": [null, null, null]
},
{
"row": ["李雷","韩梅梅", "小智"],"meta": [null, null, null]
},
{
"row": ["李雷", "韩梅梅", "小霞"],"meta": [null, null, null]
}
]

filtered query V2

{"repeat": [
{
"operator": "outV","edge_filter": {"property_filter": {"leftvalue": {"label_name": "labelName"},"predicate": "=","rightvalue": {"value": "friend"}
}
}
}
],
"times": 2,"vertices": ["李雷"],"by": [{"id": true},{"id": true},{"id": true}],"select": ["v0", "v1", "v2"]
}
{
"select": [
[
"李雷", "小明","小智"],
[
"李雷","韩梅梅", "小智"],
[
"李雷", "韩梅梅", "小霞"]
]
}

案例二:自环写法案例

gremlin

gremlin>g.V("李雷").outE('friend').has('name','xx').otherV().where(out('friend').
(hasId(
'李雷'))).limit(100)

cypher

match (a:default)-[r1:friend]->(b)-[r2:friend]->(c) where a.mid='李雷' and r1.name='xx' and a=c return id(b) limit 100

reference

LDBC Social Network Benchmark (LDBC SNB)

从零开始学Graph Database(1)- 基础篇-云社区-华为云

Filtered-query V2(2.3.6)_图引擎服务 GES_API参考_业务面API_华为云

图引擎服务GES

点击关注,第一时间了解华为云新鲜技术~

作者:京东物流 赵勇萍

前言

上个月我负责的系统SSO升级,对接京东ERP系统,这也让我想起了之前我做过一个单点登录的项目。想来单点登录有很多实现方案,不过最主流的还是基于CAS的方案,所以我也就分享一下我的CAS实践之路。

什么是单点登录

单点登录的英文名叫做:Single Sign On(简称SSO)。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。之前我做的系统,需要需要设计一套支持单点登录的鉴权认证系统,所有系统都基于一套鉴权系统进行登录,并且可以实现各个系统之间的互信和跳转。所以就采用了CAS架构。

什么是CAS

CAS架构的核心是需要搭建一个CAS Server,该服务独立部署,拥有独立三级域名,主要负责对用户的认证工作。他主要组成包括WEB前端提供登录页面,票据模块,认证模块。

核心票据:

a.
TGT(Ticket Grangting Ticket)
:TGT是CAS为用户签发的登录票据,有TGT就表明用户在CAS上成功登录过。用户在CAS认证成功后,会生成一个TGT对象,放入自己的缓存中(Session),同时生成TGC以cookie的形式写入浏览器。当再次访问CAS时,会先看cookie中是否存在TGC,如果存在则通过TGC获取TGT,如果获取到了TGT则代表用户之前登录过,通过TGT及访问来源生成针对来源的ST,用户就不用再次登录,以此来实现单点登录。

b.
TGC(Ticket-granting cookie)
:TGC就是TGT的唯一标识,以cookie的形式存在在CAS Server三级域名下,是CAS Server 用来明确用户身份的凭证。

c.
ST(Service Ticket)
:ST是CAS为用户签发的访问某一客户端的服务票据。用户访问service时,service发现用户没有ST,就会重定向到 CAS Server 去获取ST。CAS Server 接收到请求后,会先看cookie中是否存在TGC,如果存在则通过TGC获取TGT,如果获取到了TGT则代表用户之前登录过,通过TGT及访问来源生成针对来源的ST。用户凭借ST去访问service,service拿ST 去CAS Server 上进行验证,验证通过service生成用户session,并返回资源。

基于CAS的系统实践方案

1. 业务背景

在我负责的项目系统中,后台业务采用的是微服务架构,有统一的业务网关,所以基于统一的业务网关,整合客户其他系统登录鉴权流程。具体业务架构图如下:

在此说明一下,因为登录系统的用户体系在不同的系统中,所以我在设计SSO统一登录认证的时候,把SSO系统与业务系统结构出来。而用户体系有两套,一套叫做采方用户体系,一套叫做供方用户体系。所以才会有如图所示的SSO Server服务,他本身不负责用户管理,但会通过统一标准接口的方式实现控制反转,实现对用户服务的调用。

2. 单点登录时序图

时序图如下:

如图所示,时序图标识的是两个系统通过SSO服务,实现了单点登录。

3. 单点登录核心接口说明

3.1 sso认证跳转接口

调用说明:

由应用侧发起调用认证中心的接口。

URL地址:

https:// sso.com?appId=***&tenantType=1&redirectUri=***

请求方式:302重定向

参数说明:

appId: 对接SSO认证中心的应用唯一标识,由SSO认证中心通过线下的方式颁发给各个应用系统。

tenantType: 标记是供方登录还是采方登录。采方为1,供方为2.

RedirectUri: 应用回调地址。

3.2 重定向获取临时令牌code接口

调用说明:

有认证中心发起,应用侧需实现的接口。认证中心通过302重定向,将code传给应用侧,应用侧自行发起通过临时令牌code换取accessTokenInfo。

URL地址:

https://应用域名?code=***

请求方式:GET

参数说明:

Code: 临时令牌,有效时间5min

3.3 获取accessTokenInfo接口

调用说明

由应用侧发起调用认证中心的接口。通过该接口可以获取accessTokenInfo信息,然后系统自行生成本系统session信息。

URL地址:

https://sso.com/api/token/create?grantType=authorization_code&appId=yuncai&code=***

请求方式:GET

参数说明:

appId: 对接SSO认证中心的应用唯一标识,由SSO认证中心通过线下的方式颁发给各个应用系统。

code: 临时令牌,需加密

加密规则如下:

  1. Code先进行base64加密

  2. 用认证中心给的privateKey进行加密(RSA加密)。

  3. 加密后进行URLCode转码。

返回参数:

{
  “accessToken”:  “****”,  //token令牌
  “expiresIn”: 7200,        //过期时间
  “user”: {
    “username”: “zhangsan”,
       “fullName”: “张三”,
      “userId”: “1212”,
      “phone”: “13100000000”,
      “email”: zhangsan@test.com,
      “tenantId”: “S2131123”,
      “tenantType”: 1
  }
}


3.4 刷新Token接口

调用说明:

由应用侧发起调用认证中心的接口。当token快到失效期时,通过该接口可以刷新accessTokenInfo信息,然后系统自行生成本系统session信息。

URL地址:

https://sso.com/api/token/refresh?appId=yuncai&accessToken=***

请求方式:GET

参数说明:

appId: 对接SSO认证中心的应用唯一标识,由SSO认证中心通过线下的方式颁发给各个应用系统。

accessToken: 需要刷新的token值。

4. 单点登出逻辑

有单点登录,也会有单点登出,这样才会形成业务闭环,对于单点登出逻辑,基本类似登录的逆操作,时序图如下:

5. 单点登出核心接口说明

5.1 登出sso认证中心跳转接口

调用说明:

由应用侧发起调用认证中心的接口。

URL地址:

https://sso.com/logout?redirectUri=***

请求方式:GET

参数说明

RedirectUri: 应用回调地址。

5.2 应用系统退出接口

调用说明

有认证中心发起,应用侧需实现的接口。通过该接口触发个应用系统清除缓存和session相关信息,实现系统登出。

URL地址:

https://应用系统域名/ssoLogout

请求方式:GET

 header: logoutRequest:=accessToken

总结

对于CAS这种单点登录的架构,他是非常依赖于cookie的安全性的。所以CAS的安全性也在一定程度上取决于cookie的安全性,所有增强cookie安全性的措施,对于增强CAS都是有效的。

最后提一句,一定要使用HTTPS协议哦。

scikit-learn 中 Boston Housing 数据集问题解决方案

在部分旧教程或教材中是 sklearn,现在【2023】已经变更为 scikit-learn

  • 作用:开源机器学习库,支持有监督和无监督学习。它还提供了用于模型拟合、数据预处理、模型选择、模型评估和许多其他实用程序的各种工具。
  • 安装
    pip install scikit-learn

Boston Housing 数据集

此数据集原本应该在 sklearn 中是自带数据集之一,但在 scikit-learn 1.2 版本由于某些特殊原因被移除,所以无法使用
load_boston()
获取

解决办法:既然自带的数据集没有 Boston Housing,那就想办法在网上找到开放式公共数据集,下载后加载到程序中。这也是网上常见的解决方案,大多借助 pandas, scipy, numpy 等方法下载,然后标准化加载数据,供 scikit-learn 使用。

我将表述一下我所有使用的方法:通过从 openml.org 存储库下载数据集,我直接使用
fetch_openml()

from sklearn.datasets import fetch_openml

data_x, data_y = fetch_openml(name="boston", version=1, as_frame=True, return_X_y=True, parser="pandas")
  • 其中 name 是数据集在 openml.org 上的名称
  • version 是版本号,根据 openml.org 上的描述,使用 1 版本是原始数据集,所以我选择 1 版本,具体根据对应数据集的描述选择
  • as_frame=True 表示返回 pandas 的 DataFrame 格式,这样可以直接使用 pandas 的方法进行数据处理
  • return_X_y 表示分别返回特征和标签,如果为 False 则返回一个字典【包含特征和标签】,如果你想要的是字典格式,可以设置为 False,而且默认也是 False
  • parser 表示用于加载 ARFF 文件的解析器,默认的是 liac-arff
  • 更复杂的参考官方文档:
    https://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_openml.html#sklearn.datasets.fetch_openml

对 as_frame 分不分,看下面的内容你应该会有熟悉感觉,一般在分配训练数据和测试数据时都是下面步骤,我实验需求决定,所以我直接使用 as_frame=True 获取我想要的数据,如果你需要完整的,可以不使用 as_frame=True

from sklearn.model_selection import train_test_split

train_x, test_x, train_y, test_y = train_test_split(data_x, data_y, test_size=0.3, random_state=1001)

其他问题

使用上面可能会遇见一些问题【
TypeError: can't multiply sequence by non-int of type 'float'
】,一般是数据集格式问题,我在使用中是使用 numpy 进行调整的

import numpy as np
from sklearn import linear_model

model = linear_model.LinearRegression()
model.fit(train_x, train_y)
pred_y = model.predict(test_x.astype(np.float64))
  • 像是 predict 运算时,需要将 test_x 转换为 np.float64 类型,反正报错时会提醒你使用什么格式的数据,根据情况进行转换就可以了

上面加载数据集时我使用 parser="pandas" 也是为了避免,sklearn 中有时对 pandas 数据格式的需求

总结

想办法获取远程或离线的数据集,通过 scikit-learn 自带工具或其他工具【pandas, scipy, numpy 等】加载即可使用,在使用时注意不同情况下使用的数据格式并做出对应调整。

scikit-learn 适用于存储为 numpy 数组或 scipy 稀疏矩阵的任何数字数据,因为 scikit-learn 开发中也使用这些工具。比如在上面的报错中有部分内部代码涉及
np
,所以使用 numpy 转化格式就解决了报错问题。

File /opt/conda/envs/education/lib/python3.8/site-packages/sklearn/utils/extmath.py:189, in safe_sparse_dot(a, b, dense_output)
    187         ret = np.dot(a, b)
    188 else:
--> 189     ret = a @ b

前言:

本文作为解决如何通过 Golang 来编写 Web 应用这个问题的前瞻,对 Golang 中的 Web 基础部分进行一个简单的介绍。目前 Go 拥有成熟的 Http 处理包,所以我们去编写一个做任何事情的动态 Web 程序应该是很轻松的,接下来我们就去学习了解一些关于 Web 的相关基础,了解一些概念,以及 Golang 是如何运行一个 Web 程序的。
文章预计分为四个部分逐步更新
2023-04-13 星期四 一更 全文共计约 3800 字 阅读大约花费 5 分钟


文章目录:

  1. Web 的工作方式
  2. 用 Go 搭建一个最简单的 Web 服务
  3. 了解 Golang 运行 web 的原理
  4. Golang http 包详解(源码剖析)
  5. 总结

正文:

Web 的工作方式


  • 了解当你访问一个网页时,背后是如何运作的,发生了一些什么?

    其实当你输入网址(URL)并按下回车之后,你的浏览器相当于扮演了
    客户端
    的角色,首先你的浏览器会去请求 DNS 服务器进行域名解析,将你输入的 URL 地址 转化为对应的 ip 地址,通过 ip 地址 我们就可以找到对应的服务器位置,从而进行 TCP 连接。

    连接之后,你的浏览器会发送 HTTP 请求 (Request)包,服务区收到请求包之后开始从响应处理请求包,调用自身的服务,并返回 HTTP 响应 (Response)包。


    客户端收到了来自服务端的响应之后开始渲染这个响应包里的主体(body),等收到了全部的内容应答后,就会断开与服务器的 TCP 连接 (这里先讨论一般情况)
    如图