2023年4月

前言

AWS是世界上最大的云服务提供商,它提供了很多组件供消费者使用,其中进行访问控制的组件叫做IAM(Identity and Access Management), 用来进行身份验证和对AWS资源的访问控制。

功能

IAM的功能总结来看,主要分两种:

  • 验证身份(Authentication)

  • 授权访问(Authorization)

验证身份

验证身份的主要目的就是验证你的身份。

主要的身份实体有3种:

  • 用户(user),实体创建的用户,与用户组的关系为多对多

  • 用户组(group),根据一定规则分类的抽象集合,与用户的关系为多对多

  • 角色(role),其余AWS资源,例如EC2实例、Lambda函数等

对于用户来说,我们在控制台看到的是一个用户名,实际上在后台,它是一串资源字符串:

arn:aws:iam::account-ID-without-hyphens:user/User-name

确认方式有以下几种:

  1. AWS管理控制台,使用username/password方式进行认证

  2. AWS命令行工具,使用Access Key/Secret Key进行认证

  3. AWS产品开发包(SDK),使用Access Key/Secret Key进行认证

  4. Restful API,使用Access Key/Secret Key进行认证

设定权限

对于AWS来说,这部分是通过Policy来实现的。

Policy规定了被认证的实体可以访问什么权限,怎样访问权限的问题,主要由Statement来完成。而Statement是使用json格式来填写的。

针对不同的层级,我们将Policy分为两种:

  1. 针对已认证用户的层级,我们称为“Identified-Based Policy”

  2. 针对资源层级,我们称为“Resource-Based Policy”

Statement的写法如下:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListAllMyBuckets",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation"
       ],
      "Resource": "arn:aws:s3:::productionapp"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::productionapp/*"
    }
  ]
}

Identified-Based Policy

这里的Policy是针对被验证过用户的层级(此处的用户包含我上面讲的User、Group、Role)。

Policy和Statement是一对多的关系,也就是说,一个Policy可以包含多个Statement。

而Statement又包含以下内容:

  1. Effect,决定你能不能访问(Allow/Deny)

  2. Action,允许你对服务做什么动作

  3. Resource,指明这次的Statement是对哪个资源做动作

Resource-Based Policy

这里的Policy是针对资源本身的层级。

Policy和Statement是一对多的关系,也就是说,一个Policy可以包含多个Statement。

而Statement又包含以下内容:

  1. Effect,决定你能不能访问(Allow/Deny)

  2. Action,允许你对服务做什么动作

  3. Resource,指明这次的Statement是对哪个资源做动作,由于是针对自己的,所以要加上self

  4. Principle,将自己的资源套用给谁

Identified-Based Policy和Resource-Based Policy的区别

  1. Identified-Based Policy是Policy层级的,而Resource-Based Policy是Statement层级的,Identified-Based Policy比Resource-Based Policy高了一级

  2. Identified-Based Policy是从用户角度来看待权限管理的,而Resource-Based Policy是从资源角度来看待权限管理的。

小结

  1. IAM是用来做什么的

  2. 用户、用户组、角色的介绍

  3. Policy的介绍,Identified-Based Policy和Resource-Based Policy的介绍和对比

参考与鸣谢

官网 IAM

01 IAM 是做什么的? - YouTube

圖解AWS教學 - IAM - 整體架構 入門介紹 - YouTube

前言:
https://www.cnblogs.com/LoveBB/p/17277662.html

什么是枚举

枚:量词。一般用于较小的片状物,相当于“个”。

举:提出:列举。举一反三。举个例子。

所以,枚举就是一个个列举出来

枚举的作用

魔功的作用,就不过多描述了,主打的就是一个优雅。

枚举的使用

枚举的成员方法

在学习枚举之前,我们很有必要了解他的几个比较重要的成员方法。我们先定义一个枚举如下:

public enum TestEnum {
}
  • 这里可以看到,枚举中有两个比较重要的方法,分别是 values() 和 valueOf();
方法名称 具体含义 使用方法
values() 该方法可以将枚举类型成员以数组的形式返回 枚举类型名称.values()
valueOf() 该方法可以实现将普通字符串转换为枚举实例 枚举类型名称.valueOf("ABC")
  • 下面我们随便在枚举中添加两个属性 boy 和 girl
// 规范写法
public enum TestEnum {
    BOY,GIRL
}

// 属性大写只是一种规范,小写也是可以的
public enum TestEnum {
    Boy,Girl
}

// 既然小写可以,那么乱写也是没有问题的
public enum TestEnum {
    BoY,giRL
}
  • 我们使用规范的方法,试试枚举中的 values() 方法,这个方法是拿到 TestEnum 中的所有属性,并且以数组的形式返回。


  • 试试枚举中的 values() 方法

从上面可以看到,这三者的返回值是一样的,类型都是 TestEnum。那这个方法就没啥好看的了,就是拿到 TestEnum 中的单个属性。

枚举与常量类的对比

枚举类

public enum TestEnum {
    BOY,GIRL
}

常量类

public class TestConstant {
    private static Integer BOY;
    private static Integer GIRL;
}
  • 枚举和常量类的对比一目了然。但是一般情况下,我们使用常量类都是下面这种形式。
public class TestConstant {
  	// 男
    private static final Integer BOY = 0;
    // 女
    private static Integer GIRL = 1;
}
  • 这样看起来是不是更加亲切了,那么枚举类应该怎么写呢,效果如下
@Getter
public enum TestEnum {
    BOY(0),GIRL(1);
    private Integer code;
    TestEnum(Integer code){
        this.code = code;
    }

}
  • 两者使用对比
public static void main(String[] args) {
    // 使用常量类
    Integer boy = TestConstant.BOY;
    Integer girl = TestConstant.GIRL;

    // 使用枚举
    Integer enumBoy = TestEnum.BOY.getCode();
    Integer girlCode = TestEnum.GIRL.getCode();
}

枚举与常量类的使用

  • 众所周知,我们对前端展示的 性别 这个属性,一般在数据表中都是存 0,1。这样的话,每次查询和入库都需要做转换。
@Data
public class People {
   private String gender;
}
  • 常量类转化
People people = new People();
// int 转为 String
Integer boy = TestConstant.BOY;
if (boy == 0) {
    people.setGender("男");
}
Integer girl = TestConstant.GIRL;
if (girl == 1) {
    people.setGender("女");
}
  • 枚举转化
People people = new People();
// 使用枚举
Integer enumBoy = TestEnum.BOY.getCode();
if (enumBoy == 0) {
    people.setGender("男");
}
Integer girlCode = TestEnum.GIRL.getCode();
if (girlCode == 1) {
    people.setGender("女");
}

毫无违和感,那还要枚举干啥勒。别着急,下面就介绍枚举的另一种用法 :

@Getter
public enum TestEnum {
    BOY(0,"男"),GIRL(1,"女");
    private Integer code;
    private String name;
    TestEnum(Integer code,String name){
        this.code = code;
    }
}
  • 枚举转化 2
People people = new People();
// 使用枚举
Integer enumBoy = TestEnum.BOY.getCode();
if (enumBoy == 0) {
    String name = TestEnum.BOY.getName();
    people.setGender(name);
}

Integer girlCode = TestEnum.GIRL.getCode();
if (girlCode == 1) {
    String name = TestEnum.GIRL.getName();
    people.setGender(name);
}

真好,如果工资是按代码行数发的,那你就能多拿两打工资了

  • 枚举转化3

上面 2 的写法,基本上都是固定了,不符合面向对象的写法,我们应该抽象出来这一个方法,方法的入参是 int 类型,返回值是 String 类型。比如传入一个 0,返回一个男,传入一个 1,返回一个女。这个时候前面的这个 values() 方法就派上用场了

@Getter
public enum TestEnum {
    BOY(0,"男"),GIRL(1,"女");
    private Integer code;
    private String name;
    TestEnum(Integer code,String name){
        this.code = code;
    }

    // 这个是抽象的方法,放在枚举里面 
    public static String getName(int code) {
        // 遍历 TestEnum.values()
        for (TestEnum testEnum : TestEnum.values()) {
            // 如果枚举 code 和传入的 code相同,返回对应的文字
            if (testEnum.getCode() == code) {
                return testEnum.name;
            }
        }
        // 不匹配,返回默认值
        return null;
    } 

}
  • 使用方法改造
People people = new People();
// 使用枚举
Integer enumBoy = TestEnum.BOY.getCode();
if (enumBoy == 0) {
    people.setGender(TestEnum.getName(0));
}

Integer girlCode = TestEnum.GIRL.getCode();
if (girlCode == 1) {
    people.setGender(TestEnum.getName(1));
}

是不是有高手那味了,这就是魔功枚举的强大之处,随随便便一个常量,就硬生生给你整成高手范。


枚举的进阶

  • 完了嘛?如果只是简单的使用,确实是完了,但是我们的追求远不止如此,我们用了枚举,就一定要用出高手风范:一切皆可枚举

三属性枚举

要获取每个网站的地址,要把每个网站的地址存储起来,网站地址都是已知的。并且不做维护。

  • 如果要建立一张表,大概如下:

  • 如果我们用枚举来实现,就是下面这样
@Getter
public enum InternetEnum {
    BAIDU(1,"百度","www.baidu.com"),
    HUOHU(2,"火狐","www.huohu.com"),
    GUGLE(3,"谷歌","www.guge.com"),
    SLO(4,"360","www.360.com"),
    QQ(5,"qq","www.qq.com");
    private Integer code;
    private String name;
    private String address;
    private InternetEnum(int code,String name,String address) {
    this.code = code;
    this.name = name;
    this.address = address;
    }

    private String getName(int code){
        for (InternetEnum internetEnum : InternetEnum.values()) {
            if (internetEnum.getCode()==code){
                return internetEnum.getName();
            }
        }
        return null;
    }

    private String getAddress(int code){
        for (InternetEnum internetEnum : InternetEnum.values()) {
            if (internetEnum.getCode()==code){
                return internetEnum.getAddress();
            }
        }
        return null;
    }
}

这里只是举个例子,前提是这些属性不经常变的,如果是经常变的,还是得考虑使用数据表。至于哪些是不变的,如果你维护过别人的代码,就一定能见过很多这样的东西,这时候大胆使用枚举改掉他。

N属性

  • 有了三属性,就有四属性,五属性,不知道,当你看到下面这种枚举的时候,你会不会陷入沉思,并且优雅亲切来上一句问候

枚举神奇之处

  • 枚举用顺手之后,万物皆可枚举
  • 对前端:传 0,1 入参就行
  • 对数据表,咱就存 0,1 就行
  • 这样一来,主动权就在你手里了,当别人看着数据表一堆 0,1 陷入沉思的时候、当前端其他人来接手你的代码时,前端传给他一堆 0,1的时候,就是魔功显示威力的时候。

更多技术交流、求职机会,欢迎关注
字节跳动数据平台微信公众号,回复【1】进入官方交流群

背景

  • DataLeap 作为一站式数据中台套件,汇集了字节内部多年积累的数据集成、开发、运维、治理、资产、安全等全套数据中台建设的经验,助力企业客户提升数据研发治理效率、降低管理成本。

  • Data Catalog 是一种元数据管理的服务,会收集技术元数据,并在其基础上提供更丰富的业务上下文与语义,通常支持元数据编目、查找、详情浏览等功能。目前 Data Catalog 作为火山引擎大数据研发治理套件 DataLeap 产品的核心功能之一,经过多年打磨,服务于字节跳动内部几乎所有核心业务线,解决了数据生产者和消费者对于元数据和资产管理的各项核心需求。

  • Data Catalog 系统的存储层,依赖 Apache Atlas,传递依赖 JanusGraph。JanusGraph 的存储后端,通常是一个 Key-Column-Value 模型的系统,本文主要讲述了使用 MySQL 作为 JanusGraph 存储后端时,在设计上面的思考,以及在实际过程中遇到的一些问题。

起因

实际生产环境,我们使用的存储系统维护成本较高,有一定的运维压力,于是想要寻求替代方案。在这个过程中,我们试验了很多存储系统,其中 MySQL 是重点投入调研和开发的备选之一。

另一方面,除了字节内部外,在 ToB 场景,MySQL 的运维成本也会明显小于其他大数据组件,如果 MySQL 的方案跑通,我们可以在 ToB 场景多一种选择。

基于以上两点,我们投入了一定的人力调研和实现基于 MySQL 的存储后端。

方案评估

在设计上,JanusGraph 的存储后端是可插拔的,只要做对应的适配即可,并且官方已经支持了一批存储系统。结合字节的技术栈以及我们的诉求,做了以下的评估。

各类存储系统比较

  • 因投入成本过高,我们不接受自己运维有状态集群,排除了 HBase 和 Cassandra;

  • 从当前数据量与将来的可扩展性考虑,单机方案不可选,排除了 BerkeleyDB;

  • 同样因为人力成本,需要做极大量开发改造的方案暂时不考虑,排除了 Redis。

最终我们挑选了 MySQL 来推进到下一步。

MySQL 的理论可行性

  • 可以支持 Key-Value(后续简称 KV 模型)或者 Key-Column-Value(后续简称 KCV 模型)的存储模型,聚集索引 B+树排序访问,支持基于 Key 或者 Key-Column 的 Range Query,所有查询都走索引,且避免内存中重排序,效率初步判断可接受。

  • 中台内的其他系统,最大的 MySQL 单表已经到达亿级别,且 MySQL 有成熟的分库分表解决方案,判断数据量可以支持。

  • 在具体使用场景中,对于写入的效率要求不高,因为大量的数据都是离线任务完成,判断 MySQL 在写入上的效率不会成为瓶颈。

总体设计

  • 维护一张 Meta 表做 lookup 用,Meta 表中存储租户与 DataSource(库)之间的映射关系,以及 Shards 等租户级别的配置信息。

  • StoreManager 作为入口,在 openTransaction 的时候将租户信息注入到 StoreTransaction 中,并返回租户级别的 DataSource。

  • StoreManager 中以 name 为 Key,维护一组 Store,Store 与存储的数据类型有关,具有跨租户能力

    常见的 Store 有
    system_properies

    tx_log

    graphindex

    edgestore

  • 对于 MySQL 最终的读写,都收敛在 Store,方法签名中传入 StoreTransaction,Store 从中取出租户信息和数据库连接,进行数据读写。

  • 对于单租户来说,数据可以分表(shards),对于某个特定的 key 来说,存储和读取某个 shard,是根据 ShardManager 来决定

    典型的 ShardManager 逻辑,是根据总 shard 数对 key 做 hash 决定,默认单分片。

  • 对于每个 Store,表结构是 4 列(id, g_key, g_column, g_value),除自增 ID 外,对应 key-column-value model 的数据模型,key+column 是一个聚集索引。

  • Context 中的租户信息,需要在操作某个租户数据之前设置,并在操作之后清除掉。

细节设计与疑难问题

细节设计

存储模型

JanusGraph 要求 column-family 类型存储(如 Cassandra, HBase),也就是说,数据存储由一系列行组成,每行都由一个键(key)唯一标识,每行由多个列值(column-value)对组成,也会对列进行排序和过滤,如果是非 column-family 的类型存储,则需要另行适配,适配时数据模型有两种方式:Key-Column-Value 和 Key-Value。

KCV 模型:

  • 会将 key\column\value 在存储中区分开来。

  • 对应的接口为:
    KeyColumnValueStoreManager

KV 模型:

  • 在存储中仅有 key 和 value 两部分,此处的 key 相当于 KVC 模型中的 key+column;

  • 如果要根据 column 进行过滤,需要额外的适配工作;

  • 对应的接口为:
    KeyValueStoreManager
    ,该接口有子类
    OrderedKeyValueStoreManager
    ,提供了保证查询结果有序性的接口;

  • 同时提供了
    OrderedKeyValueStoreManagerAdapter
    接口,用于对 Key-Column-Value 模型进行适配,将其转化为 Key-Value 模型。

MySQL 的存储实现采用了 KCV 模型,每个表会有 4 列,一个自增的 ID 列,作为主键,同时还有 3 列分别对应模型中的 key\column\value,数据库中的一条记录相当于一个独立的 KCV 结构,多行数据库记录代表一个点或者边。

表中 key 和 column 这两列会组成联合索引,既保证了根据 key 进行查询时的效率,也支持了对 column 的排序以及条件过滤。

多租户

存储层面:默认情况下,JanusGraph 会需要存储
edgestore
,
graphindex
,
system_properties
,
txlog
等多种数据类型,每个类型在 MySQL 中都有各自对的表,且表名使用租户名作为前缀,如
tenantA_edgestore
,这样即使不同租户的数据在同一个数据库,在存储层面租户之间的数据也进行了隔离,减少了相互影响,方便日常运维。(理论上每个租户可以单独分配一个数据库)

具体实现:每个租户都会有各自的 MySQL 连接配置,启动之后会为各个租户分别初始化数据库连接,所有和 JanusGraph 的请求都会通过 Context 传递租户信息,以便在操作数据库时选择该租户对应的连接。

具体代码:

  • MysqlKcvTx:实现了
    AbstractStoreTransaction
    ,对具体的 MySQL 连接进行了封装,负责和数据库的交互,它的
    commit

    rollback
    方法由封装的 MySQL 连接真正完成。

  • MysqlKcvStore:实现了
    KeyColumnValueStore
    ,是具体执行读写操作的入口,每一个类型的 Store 对应一个
    MysqlKcvStore
    实例,
    MysqlKcvStore
    处理读写逻辑时,根据租户信息完全自主组装 SQL 语句,SQL 语句会由
    MysqlKcvTx
    真正执行。

  • MysqlKcvStoreManager:实现了
    KeyColumnValueStoreManager
    ,作为管理所有 MySQL 连接和租户的入口,也维护了所有 Store 和
    MysqlKcvStore
    对象的映射关系。在处理不同租户对不同 Store 的读写请求时,根据租户信息,创建
    MysqlKcvTx
    对象,并将其分配给对应的
    MysqlKcvStore
    去执行。

public classMysqlKcvStoreManager implements KeyColumnValueStoreManager {

@Override
publicStoreTransaction beginTransaction(BaseTransactionConfig config) throws BackendException {
String tenant
=TenantContext.getTenant();if (!tenantToDataSourceMap.containsKey(tenant)) {try{//初始化单个租户的DataSource initSingleDataSource(tenant);
}
catch(SQLException e) {
log.error(
"init mysql database source failed due to", e);throw new BackendSQLException(String.format("init mysql database source failed due to", e.getMessage()));
}
}
//获取数据库连接 Connection connection = tenantToDataSourceMap.get(tenant).getConnection(false);return newMysqlKcvTx(config, tenant, connection);
}
}

事务

几乎所有与 JanusGraph 的交互都会开启事务,而且事务对于多个线程并发使用是安全的,但是 JanusGraph 的事务并不都支持 ACID,是否支持会取决于底层存储组件,对于某些存储组件来说,提供可序列化隔离机制或者多行原子写入代价会比较大。

JanusGraph 中的每个图形操作都发生在事务的上下文中,根据 TinkerPop 的事务规范,每个线程执行图形上的第一个操作时便会打开针对图形数据库的事务,所有图形元素都与检索或者创建它们的事务范围相关联,在使用
commit
或者
rollback
方法显式的关闭事务之后,与该事务关联的图形元素都将过时且不可用。

JanusGraph 提供了
AbstractStoreTransaction
接口,该接口包含
commit

rollback
的操作入口,在 MySQL 存储的实现中,
MysqlKcvTx
实现了
AbstractStoreTransaction
,对具体的 MySQL 连接进行了封装,在其
commit

rollback
方法中调用 SQL 连接的
commit

rollback
方法,以此实现对于 JanusGraph 事务的支持。

public classMysqlKcvTx extends AbstractStoreTransaction {private static final Logger log = LoggerFactory.getLogger(MysqlKcvTx.class);privatefinal Connection connection;
@Getter
privatefinal String tenant;publicMysqlKcvTx(BaseTransactionConfig config, String tenant, Connection connection) {
super(config);
this.tenant =tenant;this.connection =connection;
}

@Override
public synchronized voidcommit() {try{if(Objects.nonNull(connection)) {
connection.commit();
connection.close();
}
if(log.isDebugEnabled()) {
log.debug(
"tx has been committed");
}
}
catch(SQLException e) {
log.error(
"failed to commit transaction", e);
}
}

@Override
public synchronized voidrollback() {try{if(Objects.nonNull(connection)) {
connection.rollback();
connection.close();
}
if(log.isDebugEnabled()) {
log.debug(
"tx has been rollback");
}
}
catch(SQLException e) {
log.error(
"failed to rollback transaction", e);
}
}
publicConnection getConnection() {returnconnection;
}
}

数据库连接池

Hikari 是 SpringBoot 内置的数据库连接池,快速、简单,做了很多优化,如使用 FastList 替换 ArrayList,自行研发无所集合类 ConcurrentBag,字节码精简等,在性能测试中表现的也比其他竞品要好。

Druid 是另一个也非常优秀的数据库连接池,为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防 SQL 注入,内置 Loging 能诊断 Hack 应用行为。

关于两者的对比很多,此处不再赘述,虽然 Hikari 的性能号称要优于 Druid,但是考虑到 Hikari 监控功能比较弱,最终在实现的时候还是选择了 Druid。

疑难问题

连接超时

现象:在进行数据导入测试时,服务报错" The last packet successfully received from the server was X milliseconds ago",导致数据写入失败。

原因:存在超大 table(有 8000 甚至 10000 列),这些 table 的元数据处理非常耗时(10000 列的可能需要 30 分钟),而且在处理过程中有很长一段时间和数据库并没有交互,数据库连接一直空闲。

解决办法:

  • 调整 mysql server 端的 wait_timeout 参数,已调整到 3600s。

  • 调整 client 端数据库配置中连接的最小空闲时间,已调整到 2400s。

分析过程:

  1. 怀疑是 mysql client 端没有增加空闲清理或者保活机制,conneciton 在线程池中长时间没有使用,mysql 服务端已经关闭该链接导致。尝试修改客户端 connection 空闲时间,增加 validationQuery 等常见措施,无果;

  2. 根据打点发现单条消息处理耗时过高,疑似线程卡死;

  3. 新增打点发现线程没卡死,只是在执行一些非常耗时的逻辑,这时候已经获取到了数据库连接,但是在执行那些耗时逻辑的过程中和数据库没有任何交互,长时间没有使用数据库连接,最终导致连接被回收;

  4. 调高了 MySQL server 端的 wait_timeout,以及 client 端的最小空闲时间,问题解决。

并行写入死锁

现象:线程 thread-p-3-a-0 和线程 thread-p-7-a-0 在执行过程中都出现 Deadlock。

具体日志如下:

[thread-p-3-a-0] ERROR org.janusgraph.diskstorage.mysql.MysqlKcvStore 313 - failed to insert query:INSERT INTO default_edgestore (g_key, g_column, g_value) VALUES (?,?,?) ON DUPLICATE KEY UPDATE g_value=?, params: key=A800000000001500, column=55A0, value=008000017CE616D0DD03674495
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to
get lock; tryrestarting transaction

[thread
-p-7-a-0] ERROR org.janusgraph.diskstorage.mysql.MysqlKcvStore 313 - failed to insert query:INSERT INTO default_edgestore (g_key, g_column, g_value) VALUES (?,?,?) ON DUPLICATE KEY UPDATE g_value=?, params: key=A800000000001500, column=55A0, value=008000017CE616D8E1036F3495
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to
get lock; tryrestarting transaction

[thread
-p-3-a-0] ERROR org.janusgraph.diskstorage.mysql.MysqlKcvStore 313 - failed to insert query:INSERT INTO default_edgestore (g_key, g_column, g_value) VALUES (?,?,?) ON DUPLICATE KEY UPDATE g_value=?, params: key=5000000000000080, column=55A0, value=008000017CE616F3C10442108A
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to
get lock; tryrestarting transaction

[thread
-p-7-a-0] ERROR org.janusgraph.diskstorage.mysql.MysqlKcvStore 313 - failed to insert query:INSERT INTO default_edgestore (g_key, g_column, g_value) VALUES (?,?,?) ON DUPLICATE KEY UPDATE g_value=?, params: key=5000000000000080, column=55A0, value=008000017CE61752B50556208A
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to
get lock; try restarting transaction

原因:

  1. 结合日志分析,两个线程并发执行,需要对同样的多个记录加锁,但是顺序不一致,进而导致了死锁。

  2. 55A0
    这个 column 对应的 property 是"__modificationTimestamp",该属性是 atlas 的系统属性,当对图库中的点或者边有更新时,对应点或者边的"__modificationTimestamp"属性会被更新。在并发导入数据的时候,加剧了资源竞争,所以会偶发死锁问题。

解决办法:

业务中并没有用到"__modificationTimestamp"这个属性,通过修改 Atlas 代码,仅在创建点和边的时候为该属性赋值,后续更新时不再更新该属性,问题得到解决。

性能测试

环境搭建

在字节内部 JanusGraph 主要用作 Data Catalog 服务的存储层,关于 MySQL 作为存储的性能测试并没有在 JanusGraph 层面进行,而是模拟 Data Catalog 服务的业务使用场景和数据,使用业务接口进行测试,主要会关注接口的响应时间。

接口逻辑有所裁剪,在不影响核心读写流程的情况下,屏蔽掉对其他服务的依赖。

模拟
单租户表单分片情况下,库表元数据创建、更新、查询,表之间血缘关系的创建、查询,以此反映在图库单次读写和多次读写情况下 MySQL 的表现。

整个测试环境搭建在火山引擎上,总共使用 6 台 8C32G 的机器,硬件条件如下:

测试场景如下:

测试结论

总计 10 万个表(库数量为个位数,可忽略)

在 10 万个表且模拟了表之间血缘关系的情况下,
graphindex
表的数据量已有 7000 万,
edgestore
表的数据量已有 1 亿 3000 万,业务接口的响应时间基本在预期范围内,可满足中小规模 Data Catalog 服务的存储要求。

总结

MySQL 作为 JanusGraph 的存储,有部署简单,方便运维等优势,也能保持良好的扩展性,在中小规模的 Data Catalog 存储服务中也能保持较好的性能水准,可以作为一个存储选择。

市面上也有比较成熟的 MySQL 分库分表方案,未来可以考虑将其引入,以满足更大规模的存储需求。

火山引擎 Data Catalog 产品是基于字节跳动内部平台,经过多年业务场景和产品能力打磨,在公有云进行部署和发布,期望帮助更多外部客户创造数据价值。目前公有云产品已包含内部成熟的产品功能同时扩展若干 ToB 核心功能,正在逐步对齐业界领先 Data Catalog 云产品各项能力。

点击跳转
大数据研发治理DataLeap
了解更多

技术背景

在Git的操作过程中,一般的组织内部工作模式可以在同一个仓库上的master-develop-feature不同分支上进行开发,也有一些人和外部协作者会通过Fork到自己本地的仓库进行更新的方案。但是对于Fork仓库的更新有一个问题是,如果长期在自己本地更新,有可能会逐渐脱离原始的分支,甚至会产生众多的冲突。那么为了避免在开发过程中遇到众多的冲突,建议大家在开发的过程中定期从远程的原始仓库同步过来。本文要探讨的是,如何跟远程的上游仓库进行同步。

方法一:强制拉取

第一种方法非常的简单粗暴,其实不建议使用,但是如果确实嫌麻烦的人,可以点击下图中的圈圈按钮,直接从原始仓库强制拉取。

之所以不推荐这种方法,是因为在远程拉取的过程中有可能出现冲突,但是因为采取了强制性拉取的策略,所以实际上本地开发的一些内容有可能会在拉取的过程中被覆盖。

方法二:创建远程同步

以下详细介绍下远程仓库同步的方法。首先我们从远程clone一个我们自己Fork的仓库到本地来:

$ git clone https://gitee.com/dechin/mindsponge.git
正克隆到 'mindsponge'...
Username for 'https://gitee.com': dechin
Password for 'https://dechin@gitee.com': 
remote: Enumerating objects: 7869, done.
remote: Counting objects: 100% (7869/7869), done.
remote: Compressing objects: 100% (2198/2198), done.
remote: Total 7869 (delta 5653), reused 7869 (delta 5653), pack-reused 0
接收对象中: 100% (7869/7869), 34.72 MiB | 1.15 MiB/s, 完成.
处理 delta 中: 100% (5653/5653), 完成.

进入本地目录,查看远程链接:

$ cd mindsponge/
$ git remote -v
origin	https://gitee.com/dechin/mindsponge.git (fetch)
origin	https://gitee.com/dechin/mindsponge.git (push)

可以看到,此时只跟自己Fork的两个仓库有链接,也就是只能pull或者push到origin的这个仓库上面。然后可以使用
git remote add
的方法添加原始仓库的链接:

$ git remote add -t develop remote https://gitee.com/helloyesterday/mindsponge.git

上一步的操作中我们甚至可以使用
-t
来配置与远程仓库构建链接的指定分支。此时我们再次查看远程链接:

$ git remote -v
origin	https://gitee.com/dechin/mindsponge.git (fetch)
origin	https://gitee.com/dechin/mindsponge.git (push)
remote	https://gitee.com/helloyesterday/mindsponge.git (fetch)
remote	https://gitee.com/helloyesterday/mindsponge.git (push)

我们发现多了两个名为remote的链接,这个就是我们Fork来的原始仓库。当然,要构建这种远程的链接,需要拥有远程仓库的相关操作权限,如push和fetch等。完成上述操作后,如果我们在本地执行了一些更新,并且已经
add

commit
过了,那么我们就可以向远程仓库进行push:

$ git push origin develop
$ git push remote develop

只要有权限,就两边都可以pull和push。这样一来,我们在本地开发了一定的时间之后,也可以从remote上pull下来最新的代码,在本地解决好相关的冲突之后,push到自己Fork的分支。最后在完成特性的开发之后,再提交一个Pull Request到远程remote的分支上,这样是一个比较合理的Git操作。

总结概要

通常来说,一个Git仓库由内部组织者和外部协作者共同维护。对于内部开发者而言,他们可以通过master-develop-feature模型进行代码开发,在仓库中创建相应的特性分支和开发分支,最后再合入到主分支供大家使用。而对于外部开发者而言,最好的方式是Fork一个仓库到自己本地,然后使用本文介绍的方法二去定期跟远程remote仓库进行同步,避免在长时间的开发周期中遇到一些冲突。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/git-remote.html

作者ID:DechinPhy

更多原著文章请参考:
https://www.cnblogs.com/dechinphy/

打赏专用链接:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

腾讯云专栏同步:
https://cloud.tencent.com/developer/column/91958

CSDN同步链接:
https://blog.csdn.net/baidu_37157624?spm=1008.2028.3001.5343

51CTO同步链接:
https://blog.51cto.com/u_15561675

简介

装饰器模式(Decorator Pattern)是一种结构型设计模式。将对象放入到一个特殊封装的对象中,为这个对象绑定新的行为,具备新的能力,同时又不改变其原有结构。

如果你希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。或者你用继承来扩展对象行为的方案难以实现或者根本不可行,你可以使用该模式。

作用

  1. 动态地给一个对象添加一些额外的职责,相比生成子类更为灵活。
  2. 在不想增加很多子类的情况下扩展类的能力,实现强大扩展能力。

实现步骤

  1. 创建一个基础工具接口或抽象类,设定基本的方法。
  2. 增加具体工具类实现基础接口,保持工具类的规范性。
  3. 创建一个装饰器抽象类,用于装饰具体工具,聚合基础工具,同时也可以实现基础工具的接口。
  4. 增加多个装饰器类,继承抽象类,根据需要设定装饰能力。

UML

Java代码

基础形状接口

//Shape.java 基础形状接口
public interfaceShape {voiddraw();
}

具体形状实现

//Circle.java 具体形状实现了基础形状接口
public class Circle implementsShape {

@Override
public voiddraw() {
System.out.println(
"Circle::draw()");
}
}
//Square.java 具体形状实现了基础形状接口 public class Square implementsShape {

@Override
public voiddraw() {
System.out.println(
"Square::draw()");
}
}

抽象装饰器

//ShapeDecorator.java 抽象装饰类,是否实现Shape可选
public abstract class ShapeDecorator implementsShape {//public abstract class ShapeDecorator {
   protectedShape decoratedShape;publicShapeDecorator(Shape decoratedShape) {this.decoratedShape =decoratedShape;
}
public voiddraw() {
decoratedShape.draw();
}
}

具体装饰器

//RedShapeDecorator.java 具体装饰器1
public class RedShapeDecorator extendsShapeDecorator {publicRedShapeDecorator(Shape decoratedShape) {super(decoratedShape);
}

@Override
public voiddraw() {
decoratedShape.draw();
setRedColor(decoratedShape);
}
private voidsetRedColor(Shape decoratedShape) {
System.out.println(
"RedShapeDecorator::setRedColor() " +decoratedShape.getClass().getName()
);
}
}
//ShadowShapeDecorator.java 具体装饰器2 public class ShadowShapeDecorator extendsShapeDecorator {publicShadowShapeDecorator(Shape decoratedShape) {super(decoratedShape);
}

@Override
public voiddraw() {//decoratedShape.draw(); setShadow(decoratedShape);
}
private voidsetShadow(Shape decoratedShape) {
System.out.println(
"ShadowShapeDecorator::setShadow() " +decoratedShape.getClass().getName()
);
}
}

测试调用

    /*** 装饰器模式是将一个对象放到一个装饰器对象中,执行装饰器类里的方法时,对象的行为能力得到增强。
* 先声明具体对象,然后放到装饰器,得到一个带有装饰器的新对象,该对象具备了新的能力。
*/ //声明形状 Shape circle = newCircle();
Shape square
= newSquare();//增加红色装饰 ShapeDecorator redCircle = newRedShapeDecorator(circle);
ShapeDecorator redSquare
= newRedShapeDecorator(square);
circle.draw();
redCircle.draw();
redSquare.draw();
//增加影子装饰 ShadowShapeDecorator shadowCircle = newShadowShapeDecorator(circle);
ShadowShapeDecorator shadowSquare
= newShadowShapeDecorator(square);
shadowCircle.draw();
shadowSquare.draw();

Go代码

基础形状接口

//Shape.go 基础形状接口
type Shape interface{
Draw()
GetName()
string}

具体形状实现

//Circle.go 具体形状实现了基础形状接口
type Circle struct{
}
func (c *Circle) Draw() {
fmt.Println(
"Circle::Draw()")
}
func (c *Circle) GetName() string{return "Circle"}//Square.go 具体形状实现了基础形状接口 type Square struct{
}
func (c *Square) Draw() {
fmt.Println(
"Square::Draw()")
}
func (c *Square) GetName() string{return "Square"}

抽象装饰器

//ShapeDecorator.go 抽象装饰类,是否实现Shape可选
type ShapeDecorator interface{
Draw()
}

具体装饰器

//RedShapeDecorator.go 具体装饰器1
type RedShapeDecorator struct{
DecoratedShape Shape
}
func (r *RedShapeDecorator) Draw() {
r.DecoratedShape.Draw()
r.SetRedColor(r.DecoratedShape)
}
func (r *RedShapeDecorator) SetRedColor(decoratedShape Shape) {
fmt.Println(
"RedShapeDecorator::setRedColor()" +decoratedShape.GetName())
}
//ShadowShapeDecorator.go 具体装饰器2 type ShadowShapeDecorator struct{
DecoratedShape Shape
}
func (s *ShadowShapeDecorator) Draw() {//装饰器根据需要是否调用形状的Draw方法//s.DecoratedShape.Draw() s.SetShadow(s.DecoratedShape)
}
func (s *ShadowShapeDecorator) SetShadow(decoratedShape Shape) {
fmt.Println(
"ShadowShapeDecorator::SetShadow()" +decoratedShape.GetName())
}

测试调用

  /**
* 装饰器模式是将一个对象放到一个装饰器对象中,执行装饰器类里的方法时,对象的行为能力得到增强。
* 先声明具体对象,然后放到装饰器,得到一个带有装饰器的新对象,该对象具备了新的能力。
*/ //声明形状 var circle = new(src.Circle)var square = new(src.Square)//增加红色装饰 var redCircle = &src.RedShapeDecorator{
DecoratedShape: circle,
}
var redSquare = &src.RedShapeDecorator{
DecoratedShape: square,
}
circle.Draw()
redCircle.Draw()
redSquare.Draw()
//增加影子装饰 var shadowCircle = &src.ShadowShapeDecorator{
DecoratedShape: circle,
}
var shadowSquare = &src.ShadowShapeDecorator{
DecoratedShape: square,
}
shadowCircle.Draw()
shadowSquare.Draw()

更多语言版本

不同语言实现设计模式:
https://github.com/microwind/design-pattern