From f70cdc619693ad762cf809daf0579403c341def1 Mon Sep 17 00:00:00 2001 From: OOOOlh <83326634+OOOOlh@users.noreply.github.com> Date: Thu, 20 Oct 2022 14:24:56 +0800 Subject: [PATCH] feat: add sequencer api component with snowflake algorithm (#767) Co-authored-by: seeflood Co-authored-by: Xunzhuo --- cmd/layotto/main.go | 4 + cmd/layotto_multiple_api/main.go | 4 + cmd/layotto_without_xds/main.go | 4 + components/go.mod | 1 + components/go.sum | 1 + components/sequencer/snowflake/snowflake.go | 379 ++++++++++++++++++ .../snowflake/snowflake_sequencer.go | 165 ++++++++ .../snowflake/snowflake_sequencer_test.go | 205 ++++++++++ .../sequencer/snowflake/snowflake_test.go | 84 ++++ configs/config_snowflake.json | 85 ++++ docs/_sidebar.md | 1 + .../en/component_specs/sequencer/snowflake.md | 67 ++++ docs/zh/_sidebar.md | 1 + .../zh/component_specs/sequencer/snowflake.md | 69 ++++ 14 files changed, 1070 insertions(+) create mode 100755 components/sequencer/snowflake/snowflake.go create mode 100755 components/sequencer/snowflake/snowflake_sequencer.go create mode 100755 components/sequencer/snowflake/snowflake_sequencer_test.go create mode 100644 components/sequencer/snowflake/snowflake_test.go create mode 100644 configs/config_snowflake.json create mode 100644 docs/en/component_specs/sequencer/snowflake.md create mode 100644 docs/zh/component_specs/sequencer/snowflake.md diff --git a/cmd/layotto/main.go b/cmd/layotto/main.go index 277ff01f15..aa413a1cee 100644 --- a/cmd/layotto/main.go +++ b/cmd/layotto/main.go @@ -138,6 +138,7 @@ import ( sequencer_mongo "mosn.io/layotto/components/sequencer/mongo" sequencer_mysql "mosn.io/layotto/components/sequencer/mysql" sequencer_redis "mosn.io/layotto/components/sequencer/redis" + sequencer_snowflake "mosn.io/layotto/components/sequencer/snowflake" sequencer_zookeeper "mosn.io/layotto/components/sequencer/zookeeper" // Actuator @@ -445,6 +446,9 @@ func NewRuntimeGrpcServer(data json.RawMessage, opts ...grpc.ServerOption) (mgrp runtime_sequencer.NewFactory("mysql", func() sequencer.Store { return sequencer_mysql.NewMySQLSequencer(log.DefaultLogger) }), + runtime_sequencer.NewFactory("snowflake", func() sequencer.Store { + return sequencer_snowflake.NewSnowFlakeSequencer(log.DefaultLogger) + }), ), // secretstores runtime.WithSecretStoresFactory( diff --git a/cmd/layotto_multiple_api/main.go b/cmd/layotto_multiple_api/main.go index bc0d51af5e..f733a73d54 100644 --- a/cmd/layotto_multiple_api/main.go +++ b/cmd/layotto_multiple_api/main.go @@ -146,6 +146,7 @@ import ( sequencer_mongo "mosn.io/layotto/components/sequencer/mongo" sequencer_mysql "mosn.io/layotto/components/sequencer/mysql" sequencer_redis "mosn.io/layotto/components/sequencer/redis" + sequencer_snowflake "mosn.io/layotto/components/sequencer/snowflake" sequencer_zookeeper "mosn.io/layotto/components/sequencer/zookeeper" // Actuator @@ -460,6 +461,9 @@ func NewRuntimeGrpcServer(data json.RawMessage, opts ...grpc.ServerOption) (mgrp runtime_sequencer.NewFactory("mysql", func() sequencer.Store { return sequencer_mysql.NewMySQLSequencer(log.DefaultLogger) }), + runtime_sequencer.NewFactory("snowflake", func() sequencer.Store { + return sequencer_snowflake.NewSnowFlakeSequencer(log.DefaultLogger) + }), ), // secretstores runtime.WithSecretStoresFactory( diff --git a/cmd/layotto_without_xds/main.go b/cmd/layotto_without_xds/main.go index cbce73d265..39d5937ae8 100644 --- a/cmd/layotto_without_xds/main.go +++ b/cmd/layotto_without_xds/main.go @@ -135,6 +135,7 @@ import ( sequencer_inmemory "mosn.io/layotto/components/sequencer/in-memory" sequencer_mongo "mosn.io/layotto/components/sequencer/mongo" sequencer_redis "mosn.io/layotto/components/sequencer/redis" + sequencer_snowflake "mosn.io/layotto/components/sequencer/snowflake" sequencer_zookeeper "mosn.io/layotto/components/sequencer/zookeeper" // Actuator @@ -430,6 +431,9 @@ func NewRuntimeGrpcServer(data json.RawMessage, opts ...grpc.ServerOption) (mgrp runtime_sequencer.NewFactory("in-memory", func() sequencer.Store { return sequencer_inmemory.NewInMemorySequencer() }), + runtime_sequencer.NewFactory("snowflake", func() sequencer.Store { + return sequencer_snowflake.NewSnowFlakeSequencer(log.DefaultLogger) + }), ), // secretstores runtime.WithSecretStoresFactory( diff --git a/components/go.mod b/components/go.mod index 2bf5eeec3a..a90fc3e171 100644 --- a/components/go.mod +++ b/components/go.mod @@ -16,6 +16,7 @@ require ( github.com/dapr/components-contrib v1.5.2 github.com/dapr/kit v0.0.2-0.20210614175626-b9074b64d233 github.com/go-redis/redis/v8 v8.8.0 + github.com/go-sql-driver/mysql v1.5.0 github.com/go-zookeeper/zk v1.0.2 github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 diff --git a/components/go.sum b/components/go.sum index f98632c514..e5bc21bdec 100644 --- a/components/go.sum +++ b/components/go.sum @@ -470,6 +470,7 @@ github.com/go-redis/redis/v8 v8.8.0/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqW github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= diff --git a/components/sequencer/snowflake/snowflake.go b/components/sequencer/snowflake/snowflake.go new file mode 100755 index 0000000000..27da647b20 --- /dev/null +++ b/components/sequencer/snowflake/snowflake.go @@ -0,0 +1,379 @@ +// +// Copyright 2021 Layotto Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package snowflake + +import ( + "database/sql" + "errors" + "fmt" + "math/rand" + "net" + "strconv" + "time" + + // mysql driver + _ "github.com/go-sql-driver/mysql" +) + +const ( + mysqlHost = "mysqlHost" + mysqlDatabaseName = "databaseName" + mysqlTableName = "tableName" + mysqlKeyTableName = "keyTableName" + mysqlUserName = "userName" + mysqlPassword = "password" + mysqlCharset = "utf8" + timeBits = "timeBits" + workerBits = "workerBits" + seqBits = "seqBits" + startTime = "startTime" + reqTimeout = "reqTimeout" + keyTimeout = "keyTimeout" + + defaultMysqlTableName = "layotto_sequencer_snowflake" + defaultKeyTableName = "layotto_sequencer_snowflake_key" + defaultTimeBits = 28 + defaultWorkerBits = 22 + defaultSeqBits = 13 + defaultStartTime = "2022-01-01" + defaultReqTimeout = 500 + defaultKeyTimeout = 24 +) + +type SnowflakeMetadata struct { + MysqlMetadata SnowflakeMysqlMetadata + + WorkerBits int64 + TimeBits int64 + SeqBits int64 + WorkidShift int64 + TimestampShift int64 + StartTime int64 + ReqTimeout time.Duration + KeyTimeout time.Duration + LogInfo bool +} + +type SnowflakeMysqlMetadata struct { + //ip:port + MysqlHost string + UserName string + Password string + DatabaseName string + TableName string + KeyTableName string + Db *sql.DB +} + +func ParseSnowflakeMysqlMetadata(properties map[string]string) (SnowflakeMysqlMetadata, error) { + mm := SnowflakeMysqlMetadata{} + + mm.TableName = defaultMysqlTableName + if val, ok := properties[mysqlTableName]; ok && val != "" { + mm.TableName = val + } + + mm.KeyTableName = defaultKeyTableName + if val, ok := properties[mysqlKeyTableName]; ok && val != "" { + mm.KeyTableName = val + } + + if val, ok := properties[mysqlHost]; ok && val != "" { + mm.MysqlHost = val + } else { + return mm, errors.New("mysql connect error: missing mysqlHost") + } + + if val, ok := properties[mysqlDatabaseName]; ok && val != "" { + mm.DatabaseName = val + } else { + return mm, errors.New("mysql connect error: missing database name") + } + + if val, ok := properties[mysqlUserName]; ok && val != "" { + mm.UserName = val + } else { + return mm, errors.New("mysql connect error: missing username") + } + + if val, ok := properties[mysqlPassword]; ok && val != "" { + mm.Password = val + } else { + return mm, errors.New("mysql connect error: missing password") + } + return mm, nil +} + +func Parsebits(val string, defaultVal int64) (int64, error) { + var bits int64 + var err error + if val != "" { + if bits, err = strconv.ParseInt(val, 10, 64); err != nil { + return bits, err + } + } else { + bits = defaultVal + } + return bits, nil +} + +func Parsetime(val string, defaultVal int) (int, error) { + var parsedVal int + var err error + + if val != "" { + parsedVal, err = strconv.Atoi(val) + if err != nil { + return parsedVal, err + } + } else { + parsedVal = defaultVal + } + return parsedVal, nil +} + +func ParseSnowflakeMetadata(properties map[string]string) (SnowflakeMetadata, error) { + metadata := SnowflakeMetadata{} + var err error + + metadata.MysqlMetadata, err = ParseSnowflakeMysqlMetadata(properties) + if err != nil { + return metadata, err + } + + metadata.WorkerBits, err = Parsebits(properties[workerBits], defaultWorkerBits) + if err != nil { + return metadata, err + } + + metadata.TimeBits, err = Parsebits(properties[timeBits], defaultTimeBits) + if err != nil { + return metadata, err + } + + metadata.SeqBits, err = Parsebits(properties[seqBits], defaultSeqBits) + if err != nil { + return metadata, err + } + + if metadata.TimeBits+metadata.WorkerBits+metadata.SeqBits+1 != 64 { + return metadata, errors.New("not enough 64bits") + } + + s := defaultStartTime + if val, ok := properties[startTime]; ok && val != "" { + s = val + } + var tmp time.Time + if tmp, err = time.ParseInLocation("2006-01-02", s, time.Local); err != nil { + return metadata, err + } + metadata.StartTime = tmp.Unix() + + parsedReqTimeout, err := Parsetime(properties[reqTimeout], defaultReqTimeout) + if err != nil { + return metadata, err + } + metadata.ReqTimeout = time.Duration(parsedReqTimeout) * time.Millisecond + + parsedKeyTimeout, err := Parsetime(properties[keyTimeout], defaultKeyTimeout) + if err != nil { + return metadata, err + } + metadata.KeyTimeout = time.Duration(parsedKeyTimeout) * time.Hour + + metadata.TimestampShift = metadata.WorkerBits + metadata.SeqBits + metadata.WorkidShift = metadata.SeqBits + + return metadata, nil +} + +func NewMysqlClient(meta *SnowflakeMysqlMetadata) (int64, error) { + + var workId int64 + //for unit test + if meta.Db == nil { + mysql := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=%s&parseTime=true&loc=Local", meta.UserName, meta.Password, meta.MysqlHost, meta.DatabaseName, mysqlCharset) + db, err := sql.Open("mysql", mysql) + if err != nil { + return workId, err + } + meta.Db = db + } + + var err error + createTable := fmt.Sprintf( + `CREATE TABLE IF NOT EXISTS %s + ( + ID BIGINT NOT NULL AUTO_INCREMENT COMMENT 'auto increment id', + HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name', + PORT VARCHAR(64) NOT NULL COMMENT 'port', + CREATED TIMESTAMP NOT NULL COMMENT 'created time', + PRIMARY KEY(ID) + )`, meta.TableName) + if _, err = meta.Db.Exec(createTable); err != nil { + return workId, err + } + + createKeyTable := fmt.Sprintf( + `CREATE TABLE IF NOT EXISTS %s + ( + SEQUENCER_KEY VARCHAR(64) NOT NULL COMMENT 'sequencer key', + WORKER_ID VARCHAR(64) NOT NULL COMMENT 'worker id', + TIMESTAMP VARCHAR(64) NOT NULL COMMENT 'timestamp', + UNIQUE INDEX (SEQUENCER_KEY) + )`, meta.KeyTableName) + if _, err = meta.Db.Exec(createKeyTable); err != nil { + return workId, err + } + + workId, err = NewWorkId(*meta) + return workId, err +} + +//get id from mysql +//host_name = "ip" +//port = "timestamp-random number" +func NewWorkId(meta SnowflakeMysqlMetadata) (int64, error) { + var workId int64 + ip, err := getIP() + stringIp := ip.String() + if err != nil { + return workId, err + } + + begin, err := meta.Db.Begin() + if err != nil { + return workId, err + } + + var host_name string + var port string + mysqlPort := getMysqlPort() + tableName := meta.TableName + + err = begin.QueryRow("SELECT HOST_NAME, PORT FROM "+tableName+" WHERE HOST_NAME = ? AND PORT = ?", stringIp, mysqlPort).Scan(&host_name, &port) + + //insert a new record if the records are duplicated, to avoid clock rollback problems after shutdown + for err == nil { + mysqlPort = getMysqlPort() + err = begin.QueryRow("SELECT HOST_NAME, PORT FROM "+tableName+" WHERE HOST_NAME = ? AND PORT = ?", stringIp, mysqlPort).Scan(&host_name, &port) + if err != nil && err != sql.ErrNoRows { + return workId, err + } + } + if err == sql.ErrNoRows { + _, err = begin.Exec("INSERT INTO "+tableName+"(HOST_NAME, PORT, CREATED) VALUES(?,?,?)", stringIp, mysqlPort, time.Now()) + + if err != nil { + return workId, err + } + } else { + return workId, err + } + + err = begin.QueryRow("SELECT ID FROM "+tableName+" WHERE HOST_NAME = ? AND PORT = ?", stringIp, mysqlPort).Scan(&workId) + if err != nil { + return workId, err + } + + if err = begin.Commit(); err != nil { + begin.Rollback() + return workId, err + } + return workId, nil +} + +func MysqlRecord(db *sql.DB, keyTableName, key string, workerId, timestamp int64) error { + var mysqlWorkerId int64 + var mysqlTimestamp int64 + + begin, err := db.Begin() + if err != nil { + return err + } + err = begin.QueryRow("SELECT WORKER_ID, TIMESTAMP FROM "+keyTableName+" WHERE SEQUENCER_KEY = ?", key).Scan(&mysqlWorkerId, &mysqlTimestamp) + if err == nil { + _, err = begin.Exec("UPDATE INTO "+keyTableName+"(SEQUENCER_KEY, WORKER_ID, TIMESTAMP) VALUES(?,?,?)", key, workerId, timestamp) + + if err != nil { + return err + } + } else if err == sql.ErrNoRows { + _, err = begin.Exec("INSERT INTO "+keyTableName+"(SEQUENCER_KEY, WORKER_ID, TIMESTAMP) VALUES(?,?,?)", key, workerId, timestamp) + if err != nil { + return err + } + } else { + return err + } + if err = begin.Commit(); err != nil { + begin.Rollback() + return err + } + return err +} + +func getMysqlPort() string { + currentTimeMills := time.Now().Unix() + rand.Seed(time.Now().UnixNano()) + randomData := rand.Int63n(100000) + mysqlPort := strconv.FormatInt(currentTimeMills, 10) + "-" + strconv.FormatInt(randomData, 10) + return mysqlPort +} + +func getIP() (net.IP, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 { + continue + } + if iface.Flags&net.FlagLoopback != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + return nil, err + } + for _, addr := range addrs { + ip := getIpFromAddr(addr) + if ip == nil { + continue + } + return ip, nil + } + } + return nil, errors.New("can't find ip") +} + +func getIpFromAddr(addr net.Addr) net.IP { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil || ip.IsLoopback() { + return nil + } + ip = ip.To4() + if ip == nil { + return nil + } + return ip +} diff --git a/components/sequencer/snowflake/snowflake_sequencer.go b/components/sequencer/snowflake/snowflake_sequencer.go new file mode 100755 index 0000000000..1a52bab045 --- /dev/null +++ b/components/sequencer/snowflake/snowflake_sequencer.go @@ -0,0 +1,165 @@ +// +// Copyright 2021 Layotto Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package snowflake + +import ( + "context" + "database/sql" + "errors" + "sync" + "time" + + "mosn.io/pkg/log" + + "mosn.io/layotto/components/sequencer" +) + +type SnowFlakeSequencer struct { + metadata SnowflakeMetadata + workerId int64 + db *sql.DB + mu sync.Mutex + smap map[string]chan int64 + biggerThan map[string]int64 + logger log.ErrorLogger + ctx context.Context + cancel context.CancelFunc +} + +func NewSnowFlakeSequencer(logger log.ErrorLogger) *SnowFlakeSequencer { + return &SnowFlakeSequencer{ + logger: logger, + smap: make(map[string]chan int64), + } +} + +func (s *SnowFlakeSequencer) Init(config sequencer.Configuration) error { + var err error + s.metadata, err = ParseSnowflakeMetadata(config.Properties) + if err != nil { + return err + } + //for unit test + s.metadata.MysqlMetadata.Db = s.db + + s.biggerThan = config.BiggerThan + s.ctx, s.cancel = context.WithCancel(context.Background()) + + if s.workerId, err = NewMysqlClient(&s.metadata.MysqlMetadata); err != nil { + return err + } + return err +} + +func (s *SnowFlakeSequencer) GetNextId(req *sequencer.GetNextIdRequest) (*sequencer.GetNextIdResponse, error) { + s.mu.Lock() + ch, ok := s.smap[req.Key] + //If the key appears for the first time, start a new goroutine for it. If the key doesn't appear for a long time, close the goroutine + if !ok { + ch = make(chan int64, 1000) + s.smap[req.Key] = ch + + var oldWorkerId int64 + var oldTimeStamp int64 + + timestamp := time.Now().Unix() - s.metadata.StartTime + + err := s.metadata.MysqlMetadata.Db.QueryRow("SELECT WORKER_ID, TIMESTAMP FROM "+s.metadata.MysqlMetadata.KeyTableName+" WHERE SEQUENCER_KEY = ?", req.Key).Scan(&oldWorkerId, &oldTimeStamp) + if err == nil { + if oldWorkerId == s.workerId { + timestamp = oldTimeStamp + 1 + } + } else if err != sql.ErrNoRows { + return nil, err + } + startId := timestamp<If port 3306 is occupied by other services, you need to exit other services first + +```shell +docker pull mysql:latest +docker run --name snowflake -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql +docker exec -it snowflake bash +mysql -uroot -p123456 +``` + +You need to create a new database: + +```mysql +CREATE DATABASE layotto_sequencer; +``` + +## Run layotto + +````shell +cd ${project_path}/cmd/layotto +go build +```` + +>If build reports an error, it can be executed in the root directory of the project `go mod vendor` + +Execute after the compilation is successful: + +````shell +./layotto start -c ../../configs/config_redis.json +```` + +## Run Demo + +````shell +cd ${project_path}/demo/sequencer/redis/ + go build -o client + ./client -s "sequencer_demo" +```` \ No newline at end of file diff --git a/docs/zh/_sidebar.md b/docs/zh/_sidebar.md index f2b6540b81..eff777a62e 100644 --- a/docs/zh/_sidebar.md +++ b/docs/zh/_sidebar.md @@ -89,6 +89,7 @@ - [Zookeeper](zh/component_specs/sequencer/zookeeper.md) - [MongoDB](zh/component_specs/sequencer/mongo.md) - [Mysql](zh/component_specs/sequencer/mysql.md) + - [Snowflake](zh/component_specs/sequencer/snowflake.md) - [Secret Store](zh/component_specs/secret/common.md) - [自定义组件](zh/component_specs/custom/common.md) - [如何部署、升级 Layotto](zh/operation/) diff --git a/docs/zh/component_specs/sequencer/snowflake.md b/docs/zh/component_specs/sequencer/snowflake.md new file mode 100644 index 0000000000..ec4f68f8e2 --- /dev/null +++ b/docs/zh/component_specs/sequencer/snowflake.md @@ -0,0 +1,69 @@ +# Snowflake + +## 配置项说明 + +示例:configs/config_snowflake.json + +| 字段 | 必填 | 说明 | +| ------------- | ---- | ------------------------------------------------------------ | +| mysqlHost | Y | mysql服务器地址,比如localhost:3306 | +| userName | Y | mysql用户名 | +| password | Y | mysql密码 | +| databaseName | Y | mysql数据库名 | +| tableName | N | mysql表名 | +| timeBits | N | 时间戳所占位数大小。默认为28 | +| workerBits | N | 机器id所占位数大小。默认为22 | +| seqBits | N | 序列号所占位数大小。默认为13 | +| startTime | N | 时间基点。默认为“2022-01-01” | +| reqTimeout | N | 请求id超时时间。默认为500毫秒 | +| keyTimeout | N | key命名空间超时时间。默认为24小时 | + +## 整体设计 + +雪花算法生成id的整体设计如下图: + +![img.jpg](https://www.gitlink.org.cn/api/attachments/397699) + +## 怎么启动 mysql + +如果想启动snowflake的demo,需要先用Docker启动一个mysql命令: + +>如果3306端口被其他服务占用,需要先退出其他服务 + +```shell +docker pull mysql:latest +docker run --name snowflake -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql +docker exec -it snowflake bash +mysql -uroot -p123456 +``` + +需要在mysql中新建一个数据库: + +```mysql +CREATE DATABASE layotto_sequencer; +``` + + + +## 启动 layotto + +````shell +cd ${project_path}/cmd/layotto +go build +```` + +>如果 build 报错,可以在项目根目录执行 `go mod vendor` + +编译成功后执行: + +````shell +./layotto start -c ../../configs/config_snowflake.json +```` + +## 运行 Demo + +````shell +cd ${project_path}/demo/sequencer/common/ + go build -o client + ./client -s "sequencer_demo" +```` \ No newline at end of file