解读gorm 的链式操作

go的gorm刚用起来比较玄幻。有时会触发多个db操作相互污染的问题。为了弄清这个问题特地研究了一下文档及源码。文档链接地址:https://gorm.io/zh_CN/docs/method_chaining.html
下面我尝试从源码角度理解一下gorm句柄切换的逻辑:

func (db *DB) getInstance() *DB {
    // 句柄之间的转换关键在 db.clone,通过这个标识来确定怎么操作sql(所有操作db的逻辑都存放到这个属性里:db.Statement)
    //clone > 0 时:需要新建一个db.Statement 实例,
    if db.clone > 0 {
       //这里得到的 tx.clone = 0
        tx := &DB{Config: db.Config, Error: db.Error}

        // =1 时代表需要重新定义一个句柄(新的db.Statement实例)(另外提示一下:通过db.Open 获取的句柄 db.clone = 1)
        if db.clone == 1 {
            // clone with new statement
            tx.Statement = &Statement{
                DB:       tx,
                ConnPool: db.Statement.ConnPool,
                Context:  db.Statement.Context,
                Clauses:  map[string]clause.Clause{},
                Vars:     make([]interface{}, 0, 8),
            }
        } else {// > 1时,代表需要从原来的db.Statement clone 一个,(另外提示一下:执行db.Session(&gorm.Session{}) 的时候,db.clone = 2,将用到这里的逻辑)
            // with clone statement
            tx.Statement = db.Statement.clone()
            tx.Statement.DB = tx
        }

        return tx
    }

   // db.clone = 0时,直接返回
    return db
}

可以理解为,clone > 0 时,每次第一次操作数据库后,就会返回一个新的db 句柄,这个新的句柄会保留所有在这个句柄上的操作,直至调用 db.Session(&gorm.Session{})时, 会保留之前的操作,然后会继续链上下次的操作。

在重用db句柄时要注意这个逻辑,特别在事务中,必须重用一个db实例,否则就不是在一个事务中了。这个时候就要小心sql相互污染的问题,比较好的习惯是“一链到底”,如非必要别再次赋值。如下:

//Db() 方法获取gorm.Open() 返回的单例
db := Db().Begin()
//这个db 已经和上一行的db不是一个实例了
db = db.Where("type = 2")
if s, found := getData["status"]; found {
   //这个db 和 第二个 db 是一个实例
   db = db.Where("status = ?", s)
}

优化之后的代码就看起来比较简单了

//这里换一个名字
tr := Db().Begin()
//从tr获取一个db句柄
dbDoSomething1 = tr.Where("type = 2")
if s, found := getData["status"]; found {
   //直接操作db句柄
   dbDoSomething1.Where("status = ?", s)
}
发表在 go | 标签为 , , , | 留下评论

使用docker-compose 构建go项目,实现优雅重启或者停止(二) – 离线job

使用 docker-compose + supervisor 管理go的job
supervisor 的配置文件示例如下:

[program:xxx]
process_name=%(program_name)s_%(process_num)02d
command=gocommand xxx --config xxx.yml
directory=/var/www/program #工作目录,定义工作目录是为了日志路径可以保证在项目目录下
autostart=true #自动启动
autorestart=true #自动重启
numprocs=1 #只有一个进程
stdout_logfile=/var/log/xxx.log #标准输出日志文件
stderr_logfile=/var/log/xxx_stderr.log #错误输入日志文件
redirect_stderr=true #重定向错误日志
stdout_logfile_maxbytes = 50MB #日志文件大小
stdout_logfile_backups = 20 #日志文件保留数量
stopsignal=INT #以SIGINT (信号2)通知进程结束
stopwaitsecs=6000 #等待进程结束时间,超过这个时间则强制杀死进程,默认10s

go 中代码如下:

quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, os.Kill)
<-quit
fmt.Println("监控到退出信号")

最后是docker-compose 通知 supervisor 重启,

supervisor:
    restart: always
    stop_grace_period: 6000s #等待时间,超过这个时间则强制杀死容器,默认10秒
    build:
      context: ./supervisor
    volumes:
      - ${APP_CODE_PATH_HOST}:${APP_CODE_PATH_CONTAINER}
    networks:
      - backend
发表在 go | 留下评论

使用docker-compose 构建go项目,实现优雅重启或者停止(一)- webserver

1. 在go项目中接受系统信号:

quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, os.Kill)
<-quit

2. 在docker-compose中构建添加配置 stop_signal: SIGINT,如下:

  blockchain-api:
    restart: always
    stop_signal: SIGINT
    build:
      context: ./../blockchain-api-dev
发表在 docker, go | 标签为 , | 留下评论

docker 搭建 rocketMQ

docker pull apache/rocketmq
//启用name server
docker run -d --restart=always --name rmqnamesrv --privileged=true -p 9876:9876 -v /www/docker/rocketmq/nameserver/logs:/root/logs -v /www/docker/rocketmq/nameserver/store:/root/store -e "MAX_POSSIBLE_HEAP=100000000" apache/rocketmq sh mqnamesrv

//启用broker
docker run -d --restart=always --name rmqbroker -p 10911:10911 -p 10909:10909 --privileged=true -v /www/docker/rocketmq/data/broker/logs:/root/logs -v /www/docker/rocketmq/data/broker/store:/root/store -v /www/rocketmq/broker.conf:/opt/docker/rocketmq/broker.conf -e "NAMESRV_ADDR=172.21.32.2:9876"  -e "MAX_POSSIBLE_HEAP=200000000" apache/rocketmq sh mqbroker -c /opt/docker/rocketmq/broker.conf

//启用rocketmq-dashboard
docker pull apacherocketmq/rocketmq-dashboard
docker run -d --name rocketmq-dashboard -e "JAVA_OPTS=-Drocketmq.namesrv.addr=172.21.32.2:9876" -p 80:8080 -t apacherocketmq/rocketmq-dashboard:latest

broker 配置文件
///www/rocketmq/broker.conf
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
brokerIP1 = 172.21.32.2

发表在 未分类 | 留下评论

docker 获取宿主机ip的方式

在宿主机中执行下面的命令,反馈的ip即是宿主机ip:

#ping host.docker.internal
发表在 docker | 留下评论

docker 启动时修改启动命令参数,gitlab-runner docker启动时修改用户运行用户为root

1. 通过docker inspect 【gitlab-runner】查看容器信息
2. 参考 Args 可修改默认执行参数
例如 gitlab-runner docker 默认启动参数为 --user=gitlab-runner, 要修改为root用户可使用下面的命令

docker run -d --name gitlab-runner -v /srv/gitlab-runner/config:/etc/gitlab-runner -v /root/.ssh:/root/.ssh -v /www:/www gitlab/gitlab-runner:alpine-v13.12.0 run --user=root --working-directory=/home/gitlab-runner
//最后的  run --user=root --working-directory=/home/gitlab-runner 即是修改的默认启动参数
发表在 docker, git | 标签为 , | 留下评论

node 调试用web server

//index.js
const http = require('http')
const server = http.createServer((req, res) => {
    // req即request,res即response

     //console.log('ok'); // 每次请求都会执行一次回调函数中的语句

   // 浏览器输入的 url 为 http://localhost:2000/hello?username=sense&sex=male#hello,
   // 则 req.url 为 /hello?username=sense&sex=male,
   // 浏览器输入的 url 中的 hash(如上的 #hello)无法传递到后端
   const url = req.url.split('?')[0]

   res.end(url)
   // 每次触发请求回调函数中只能调用一次response.end(),否则会报错
 })
const port = 3000;
server.listen(port)
console.log('hello port '+ port);
#node index.js
发表在 js, 前端 | 留下评论

设计模式感悟

1. 只有真正的熟悉掌握了一种设计模式的思想以及使用场景和使用方法,才能触类旁通的快速了解它的设计模式,而不是似是而非的懵懂!
2. 当你真正的熟悉的掌握了两种或3种设计模式,那么在设计代码时,其他的设计模式对你来说才“真正”的提供了一种“解决问题的思想“而已,很快的就能被你掌握和使用!
3. 当你对于一种设计模式不断加深了解的同时,你也会对设计模式的思想其他设计几种模式潜移默化的加深了解。

发表在 设计模式 | 留下评论

Yii debug 工具 dump panel 使用

1. 设置 yii debug

if (YII_ENV_DEV) {
    // configuration adjustments for 'dev' environment
    $config['bootstrap'][] = 'debug';
    $config['modules']['debug'] = [
        'class' => 'yii\debug\Module',
        // uncomment the following to add your IP if you are not connecting from localhost.
        'allowedIPs' => ['127.0.0.1'],
        'panels' => [
            //设置dump,默认的dump只接受 application 分类,vendor/yiisoft/yii2-debug/src/panels/DumpPanel.php
            'dump' => [
                'class' => 'yii\debug\panels\DumpPanel',
                'categories' => ['response'],
            ]
        ]
    ];
}

2. 使用

//yii debug dump 默认只收集debug级别的日志
\Yii::debug("hi", "response");//response 是在配置时使用的日志分类
发表在 php, yii2 | 留下评论

工厂方法模式

假设要实现一个功能,要封装访问数据库的操作。
1. 不实用设计模式的情况:

/**
 *文件列表
 *|
 *|--Database.php 数据库封装类
 *|--main.php 使用程序(客户端)
 */

/**
 * Database.php 数据库封装
 */
class Database{
    public $connect = null;
    function select(){
        //...
    }
    function insert(){
        //...
    }
    function update(){
        //...
    }

    function delete(){
        //...
    }
}

/**
 * main.php
 */
$db = new Database();
$db->select();

//缺点:耦合严重,不好扩展

2. 简单工厂

/**
 *文件列表
 *|--drivers 数据库连接封装
     |-- BaseDrivers.php 产品超类
     |-- Mysql.php 具体的产品
     |-- TiDb.php 另一个具体的产品
 *|--DbFactory.php 数据库工厂
 *|--main.php 使用程序(客户端)
 */

/**
 * DbFactory.php 数据库工厂(简单工厂,产品是指定类型的数据库操作具柄)
 */
class DbFactory
{
    public static function getDatabase($name):Database
    {
        if($name == "mysql"){
            return new Mysql();
        }else if($name = "tidb"){
            return new TiDb();
        }

        throw new \Exception("driver not find:".$name);
    }
}

/**
 * BaseDrivers.php 数据库驱动基础类(简单工厂模式中的产品的超类,定义产品接口)
 */
abstract class BaseDrivers
{
    public $connect = null;
    abstract function select();
    abstract function insert();
    abstract function update();
    abstract function delete();
}


/**
 * Mysql.php mysql 数据库驱动类(具体的产品)
 */
class Mysql extends BaseDrivers
{

    function select()
    {
        // TODO: Implement select() method.
    }

    function insert()
    {
        // TODO: Implement insert() method.
    }

    function update()
    {
        // TODO: Implement update() method.
    }

    function delete()
    {
        // TODO: Implement delete() method.
    }
}


/**
 * TiDb.php tidb数据库驱动类(另一种具体的产品)
 */
class TiDb extends BaseDrivers
{

    function select()
    {
        // TODO: Implement select() method.
    }

    function insert()
    {
        // TODO: Implement insert() method.
    }

    function update()
    {
        // TODO: Implement update() method.
    }

    function delete()
    {
        // TODO: Implement delete() method.
    }
}


/**
 * main.php
 */
$database = DbFactory::getDatabase('mysql');//获取mysql的数据库操作
$database->select();

//优点:不说了,自行百度
//缺点:如果在添加其他的数据库类型操作,必须得修改DbFactory.php,增加 if...else... ,然后才能在main中获取新的数据库操作句柄

3. 工厂方法模式

/**
 *文件列表
 *|--drivers 数据库连接封装
     |-- BaseDrivers.php 产品超类
     |-- Mysql.php 具体的产品
     |-- TiDb.php 另一个具体的产品
 *|--factories.php 工厂封装
 *   |-- BaseFactory.php 工厂超类
 *   |-- MysqlFactory.php 具体工厂(只生产mysql产品的工厂)
 *   |-- TiDbFactory.php 另一个具体的工厂(只生产TiDb产品的工厂)
 *|--main.php 使用程序(客户端)
 */

/**
 * BaseFactory.php 工厂超类
 */
interface  BaseFactory
{
    function getDb();
}


/**
 * MysqlFactory.php mysql工厂
 */
class MysqlFactory implements BaseFactory
{
    function getDb()
    {
        return new Mysql();
    }
}


/**
 * TiDbFactory.php TiDb工厂
 */
class TiDbFactory implements BaseFactory
{
    function getDb()
    {
        return new TiDb();
    }
}

/**
 * BaseDrivers.php 数据库驱动基础类
 */
abstract class BaseDrivers
{
    public $connect = null;
    abstract function select();
    abstract function insert();
    abstract function update();
    abstract function delete();
}


/**
 * Mysql.php mysql 数据库驱动类
 */
class Mysql extends BaseDrivers
{

    function select()
    {
        // TODO: Implement select() method.
    }

    function insert()
    {
        // TODO: Implement insert() method.
    }

    function update()
    {
        // TODO: Implement update() method.
    }

    function delete()
    {
        // TODO: Implement delete() method.
    }
}


/**
 * TiDb.php tidb数据库驱动类
 */
class TiDb extends BaseDrivers
{

    function select()
    {
        // TODO: Implement select() method.
    }

    function insert()
    {
        // TODO: Implement insert() method.
    }

    function update()
    {
        // TODO: Implement update() method.
    }

    function delete()
    {
        // TODO: Implement delete() method.
    }
}


/**
 * main.php
 */
$database = MysqlFactory::getDb();
$database->select();

//优点:新增数据库类型时,不用在修改框架部分代码(没有了Factory.php,不用在依赖这个类了)。只需要新添加一个对应的工厂,然后新增加一个数据库driver(产品),就可以直接在客户端(main.php)使用了,完全不影响其他的已经完成的功能。
发表在 设计模式 | 留下评论