小子 发布的文章

php扩展实践之数组操作

初始化数组

zval *arr;
MAKE_STD_ZVAL(arr);
array_init(arr);

插入和更新

// 索引
add_index_bool(arr, 10, 1);
add_next_index_long(arr, 1000);
// 关联数组
add_assoc_stringl(arr, "name", ZEND_STRL("xiaozi"), 1);

查找和删除

char *name = "name";
int nameLen = strlen(name);
// 查找
zend_hash_exists(Z_ARRVAL_P(arr), name, nameLen + 1);
// 删除
zend_hash_del(Z_ARRVAL_P(arr), name, nameLen + 1);

读取

char *name = "name";
int nameLen = strlen(name);
zval **value;
if (zend_hash_find(Z_ARRVAL_P(attributes), name, nameLen + 1, (void **)&value) == SUCCESS) {
    php_var_dump(value, 1 TSRMLS_CC);
}

php扩展实践之implements内置接口

这次用 php 扩展的方式来实现一下 Laravel5.1 Support 中的 Fluent,实现一下 php 内置接口 ArrayAccess 的方法

method参数的定义

#include "Zend/zend_interfaces.h"

zend_class_entry *fluent_ce;

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent___construct, 0, 0, 1)
    ZEND_ARG_ARRAY_INFO(0, attributes, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent_offsetexists, 0, 0, 1)
    ZEND_ARG_INFO(0, offset)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent_offsetget, 0, 0, 1)
    ZEND_ARG_INFO(0, offset)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent_offsetset, 0, 0, 2)
    ZEND_ARG_INFO(0, offset)
    ZEND_ARG_INFO(0, value)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent_offsetunset, 0, 0, 1)
    ZEND_ARG_INFO(0, offset)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent___get, 0, 0, 1)
    ZEND_ARG_INFO(0, name)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent___set, 0, 0, 2)
    ZEND_ARG_INFO(0, name)
    ZEND_ARG_INFO(0, value)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent___isset, 0, 0, 1)
    ZEND_ARG_INFO(0, name)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fluent___unset, 0, 0, 1)
    ZEND_ARG_INFO(0, name)
ZEND_END_ARG_INFO()

定义类方法

ZEND_METHOD(fluent, __construct) {
    zval *attributes, *instance;
    MAKE_STD_ZVAL(attributes);
    array_init(attributes);
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|a", &attributes) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    zend_update_property(fluent_ce, instance, ZEND_STRL("attributes"), attributes TSRMLS_DC);
}
ZEND_METHOD(fluent, offsetExists) {
    zval *value, *instance, *member;
    char *offset;
    int offsetLen;
    int exists;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &offset, &offsetLen) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    MAKE_STD_ZVAL(member);
    ZVAL_STRINGL(member, offset, offsetLen, 1);
    exists = std_object_handlers.has_property(instance, member, 0, 0 TSRMLS_DC);
    zval_ptr_dtor(&member);
    RETURN_BOOL(exists);
}
ZEND_METHOD(fluent, offsetGet) {
    zval *value, *instance;
    char *offset;
    int offsetLen;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &offset, &offsetLen) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    php_printf("%s\n", offset);
    value = zend_read_property(fluent_ce, instance, offset, offsetLen, 1 TSRMLS_CC);
    RETURN_ZVAL(value, 1, 0);
}
ZEND_METHOD(fluent, offsetSet) {
    zval *value, *instance;
    char *offset;
    int offsetLen;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz", &offset, &offsetLen, &value) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    zend_update_property(fluent_ce, instance, offset, offsetLen, value TSRMLS_DC);
}
ZEND_METHOD(fluent, offsetUnset) {
    zval *instance, *member;
    char *offset;
    int offsetLen;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &offset, &offsetLen) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    MAKE_STD_ZVAL(member);
    ZVAL_STRINGL(member, offset, offsetLen, 1);
    std_object_handlers.unset_property(instance, member, 0 TSRMLS_DC);
    zval_ptr_dtor(&member);
}
ZEND_METHOD(fluent, __get) {
    char *name;
    int nameLen;
    zval *attributes, *instance;
    zval **value;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &nameLen) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    attributes = zend_read_property(fluent_ce, instance, ZEND_STRL("attributes"), 0 TSRMLS_CC);
    if (zend_hash_find(Z_ARRVAL_P(attributes), name, nameLen + 1, (void **)&value) == SUCCESS) {
        RETURN_ZVAL(*value, 1, 0);
    } else {
        RETURN_NULL();
    }
}
ZEND_METHOD(fluent, __set) {
    char *name;
    int nameLen;
    zval *value, *attributes, *instance;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz", &name, &nameLen, &value) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    attributes = zend_read_property(fluent_ce, instance, ZEND_STRL("attributes"), 0 TSRMLS_CC);
    add_assoc_zval(attributes, name, value);
    zval_add_ref(&value);
}
ZEND_METHOD(fluent, __isset) {
    char *name;
    int nameLen;
    zval *attributes, *instance;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &nameLen) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    attributes = zend_read_property(fluent_ce, instance, ZEND_STRL("attributes"), 0 TSRMLS_CC);
    if (zend_hash_exists(Z_ARRVAL_P(attributes), name, nameLen + 1) == SUCCESS) {
        RETURN_BOOL(1);
    } else {
        RETURN_BOOL(0);
    }
}
ZEND_METHOD(fluent, __unset) {
    char *name;
    int nameLen;
    zval *attributes, *instance;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &nameLen) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    attributes = zend_read_property(fluent_ce, instance, ZEND_STRL("attributes"), 0 TSRMLS_CC);
    zend_hash_del(Z_ARRVAL_P(attributes), name, nameLen + 1);
}

static zend_function_entry fluent_methods[] = {
    ZEND_ME(fluent, __construct, arginfo_fluent___construct, ZEND_ACC_CTOR | ZEND_ACC_PUBLIC)
    ZEND_ME(fluent, offsetExists, arginfo_fluent_offsetexists, ZEND_ACC_PUBLIC)
    ZEND_ME(fluent, offsetGet, arginfo_fluent_offsetget, ZEND_ACC_PUBLIC)
    ZEND_ME(fluent, offsetSet, arginfo_fluent_offsetset, ZEND_ACC_PUBLIC)
    ZEND_ME(fluent, offsetUnset, arginfo_fluent_offsetunset, ZEND_ACC_PUBLIC)
    ZEND_ME(fluent, __get, arginfo_fluent___get, ZEND_ACC_PUBLIC)
    ZEND_ME(fluent, __set, arginfo_fluent___set, ZEND_ACC_PUBLIC)
    ZEND_ME(fluent, __isset, arginfo_fluent___isset, ZEND_ACC_PUBLIC)
    ZEND_ME(fluent, __unset, arginfo_fluent___unset, ZEND_ACC_PUBLIC)
    {NULL, NULL, NULL}
};

模块初始化的时候注册类

PHP_MINIT_FUNCTION(learn)
{
    zval *attributes;
    MAKE_STD_ZVAL(attributes);
    array_init(attributes);
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, "Fluent", fluent_methods);
    fluent_ce = zend_register_internal_class(&ce TSRMLS_CC);
    // implements ArrayAccess
    zend_class_implements(fluent_ce TSRMLS_DC, 1, zend_ce_arrayaccess);
    zend_declare_property_null(fluent_ce, ZEND_STRL("attributes"), ZEND_ACC_PROTECTED TSRMLS_CC);

    return SUCCESS;
}

php扩展实践之调用类

PHP_FUNCTION(learn_call) {
    // 实例化类
    zval *learn;
    MAKE_STD_ZVAL(learn);
    object_init_ex(learn, learn_ce);

    char *name = "xiaozi";
    char *methodName = "__construct";
    zval *param, *method, *callResult;
    zval **params[1];
    MAKE_STD_ZVAL(method);
    ZVAL_STRINGL(method, methodName, strlen(methodName), 1);
    MAKE_STD_ZVAL(param);
    ZVAL_STRINGL(param, name, strlen(name), 1);
    params[0] = &param;
    // 调用类的 __construct 方法
    call_user_function_ex(&(learn_ce)->function_table, &learn, method, &callResult, 1, params, 0, NULL TSRMLS_CC);
    zval_ptr_dtor(&method);
    zval_ptr_dtor(&param);
    zval_ptr_dtor(&learn);
    if (callResult) {
        zval_ptr_dtor(&callResult);
    }
}

php扩展实践之定义类

zend_class_entry *learn_ce;

ZEND_BEGIN_ARG_INFO_EX(arginfo_learn___construct, 0, 0, 1)
    ZEND_ARG_INFO(0, name)
ZEND_END_ARG_INFO()

// Learn::__construct 的实现
ZEND_METHOD(learn, __construct) {
    char *name = "tianzi", *methodName;
    zval *method, *callResult, *instance;
    int nameLen = strlen(name);
    // 带有默认值的传入参数
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|s", &name, &nameLen) == FAILURE) {
        RETURN_NULL();
    }
    instance = getThis();
    // 更新类的属性
    zend_update_property_stringl(learn_ce, instance, ZEND_STRL("name"), ZEND_STRL(name) TSRMLS_DC);
}
// Learn::hi 的实现
ZEND_METHOD(learn, hi) {
    zval *instance = getThis();
    // 读取类的属性
    zval *name = zend_read_property(learn_ce, instance, ZEND_STRL("name"), 0 TSRMLS_CC);
    char *theName = estrndup(Z_STRVAL_P(name), Z_STRLEN_P(name));
    php_printf("hi %s\n", theName);
    efree(theName);
}
// 类方法的属性
static zend_function_entry learn_methods[] = {
    ZEND_ME(learn, __construct, arginfo_learn___construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
    ZEND_ME(learn, hi, NULL, ZEND_ACC_PUBLIC)
    {NULL, NULL, NULL}
};

PHP_MINIT_FUNCTION(learn)
{
    zend_class_entry ce;
    INIT_CLASS_ENTRY(ce, "Learn", learn_methods);
    // 注册类
    learn_ce = zend_register_internal_class(&ce TSRMLS_CC);
    // 定义类属性
    zend_declare_property_null(learn_ce, ZEND_STRL("name"), ZEND_ACC_PUBLIC TSRMLS_CC);

    return SUCCESS;
}

php扩展实践之定义常量

PHP_MINIT_FUNCTION(learn)
{
    // CONST_CS 区分大小写
    // CONST_PERSISTENT 在模块加载的时候都是有效的,否则每次 request 之后就会删除
    REGISTER_BOOL_CONSTANT("LEARN", 1, CONST_CS | CONST_PERSISTENT);

    return SUCCESS;
}

Vuejs 和 CodeMirror 的绑定

实现 vuejs 和 codemirror 的相互绑定

Vue.directive("codemirror", {
    twoWay: true,
    bind: function () {
        // 请自行初始化CodeMirror, 可使用 $(this.el).data('codemirror')
        this.editor = editor;
        // model主动赋值标记
        this.silent = false;
        this.handler = function () {
            if (!this.silent) {
                this.set(this.editor.getValue(), true); // 加锁,防止相互赋值
            }
        }.bind(this);
        this.editor.on("change", this.handler);
    },
    update: function (value, oldValue) {
        this.silent = true;
        this.editor.setValue(this.vm.$data[this.raw]);
        this.silent = false;
    }
});

使用:

<!-- code 为 Vue data 中使用的变量名 -->
<textarea id="codepad" v-codemirror="code"></textarea>

golang中map的排序

在实现 golang 中发布订阅模式的时候,需要按照优先级排序回调函数;golang 中的 map 是无序的,需要手动取出 key,并对key进行排序,下面是排序一块的代码段:

[
    1 => [func1, func2]
    0 => [func5, func6]
    2 => [func3, func4]
]

...

[func5, func6, func1, func2, func3, func4]
func (ed *eventdispatcher) SortListeners(event string) {
    ed.sorted[event] = nil
    // 发布订阅模式中函数执行的优先级
    priorities := make([]int, 0)
    for priority, _ := range ed.listeners[event] {
        priorities = append(priorities, priority)
    }
    // 对优先级的数字进行排序
    sort.Ints(priorities)
    sorted := make([]func(interface {}) interface {}, 0)
    // 按照优先级顺序合并 map
    for _, priority := range priorities {
        sorted = append(sorted, ed.listeners[event][priority]...)
    }
    ed.sorted[event] = sorted
}

PopClip插件开发

配置文件

Actions 里面一个 dict 是一个图标,由于 PopClip 不支持直接执行可执行文件,所以要使用 shell 来执行一下。

自己给定的两个图标的颜色是没有关系的,PopClip 会自动修改图标的颜色。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Actions</key>
  <array>
    <dict>
      <key>After</key>
      <string>copy-result</string>
      <key>Image File</key>
      <string>id.png</string>
      <key>Regular Expression</key>
      <string>(?s)1\w+$</string>
      <key>Title</key>
      <string>Url2Id</string>
      <key>Shell Script File</key>
      <string>url2id.sh</string>
    </dict>
    <dict>
      <key>After</key>
      <string>copy-result</string>
      <key>Image File</key>
      <string>url.png</string>
      <key>Regular Expression</key>
      <string>(?s)\d+$</string>
      <key>Title</key>
      <string>Id2Url</string>
      <key>Shell Script File</key>
      <string>id2url.sh</string>
    </dict>
  </array>
  <key>Apps</key>
  <array>
    <dict>
      <key>Link</key>
      <string>http://tool.lu/</string>
      <key>Name</key>
      <string>在线工具</string>
    </dict>
  </array>
  <key>Credits</key>
  <array>
    <dict>
      <key>Link</key>
      <string>mailto:245565986@qq.com</string>
      <key>Name</key>
      <string>xiaozi</string>
    </dict>
  </array>
  <key>Extension Description</key>
  <string>Convert ids for mogujie.</string>
  <key>Extension Identifier</key>
  <string>lu.tool.popclip.extension.id-converter</string>
  <key>Extension Image File</key>
  <string>id.png</string>
  <key>Extension Name</key>
  <string>Id Converter</string>
  <key>Version</key>
  <integer>1</integer>
</dict>
</plist>

代码

PopClip 操作的文本是直接放在环境变量 POPCLIP_TEXT 里面的,所以下面的代码可以当做是 go 的一个插件模板

package main

import (
    "fmt"
    "os"
)

func main() {
    text := os.Getenv("POPCLIP_TEXT")
    fmt.Print(text)
}
go build -o IdConverter .

发布

mv IdConverter/ IdConverter.popclipext
zip -r IdConverter.popclipext.zip IdConverter.popclipext
mv IdConverter.popclipext.zip IdConverter.popclipextz

以库的形式调用YUI Compressor

import com.yahoo.platform.yui.compressor.CssCompressor;
import org.apache.commons.io.IOUtils;
import java.io.*;

public class YUIProcessor {
    public String compressCss(String code) {
        Reader in = new InputStreamReader(IOUtils.toInputStream(code));
        try (Writer out = new StringWriter()) {
            CssCompressor compressor = new CssCompressor(in);
            compressor.compress(out, -1);
            out.flush();
            return out.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }
}

追加:

yui compresser 很久没有更新,有很多的不足,已经废弃使用。

单锁的分拆降低冲突概率

      +-----------+
-->   | counter 1 |
      +-----------+
-->   | counter 2 |
      +-----------+   -> sum(subCounter)
-->   | counter 3 |
      +-----------+
-->   | counter 4 |
      +-----------+

JAVA 中的 ConcurrentHashMap 和 LongAdder 的应用

在内部都有多个计数器,这样在并发更新对象的时候,随机选择一个,就可以将锁冲突的概率降低到 1/n

mysql 计数器的应用

拿文章的喜欢计数来做例子,当有n个人同时点击喜欢的时候,INNODB会锁住这条记录,这篇文章的喜欢计数器将是串行增长的。

pre_posts

+-------+-----+----------+
|  id   | ... |   favs   |
+-------+-----+----------+
| 10000 | ... |   8001   |
+-------+-----+----------+

再看下面的,可以将喜欢数从 posts 表拆分出来,一个文章 id,对应多个子计数器,在更新计数器的时候,可以随机选择一条记录增加,在取文章喜欢数的时候,可以sum一下累加起来;这样就降低了并发时候的冲突。

pre_post_favs

+-----+----------+----------+
| id  | post_id  |   favs   |
+-----+----------+----------+
|  1  |  10000   |   2001   |
|  2  |  10000   |   1994   |
|  3  |  10000   |   2010   |
|  4  |  10000   |   1898   |
+-----+----------+----------+

对于计数器的缓存

显然上面的数据解决了数据并发写入的问题,但是查询的代价却是提高了,想到的办法就是对计数器进行缓存。

读取的时候被动缓存的话,就会产生计数器的延迟;所以这里可以采用 canal 监听 mysql binlog 主动触发 cache 的更新。

那么问题又来了,既然是存到 redis 中,该用什么类型的;如果使用大量的 string 的话,key 就会占用大量的内存,可以部分改用 hash,但是 redis 的设定中,只有 数量 < hash-max-ziplist-entries 的时候才会采用优化的存储方式;根据多位大牛的研究 1000 是比较合适的。

favs:10
    000 -> 8001
    001 -> 0
    ...
    999 -> 0

线上数据无缝迁库

当 数据量 和 qps/tps 达到一定的时候,将某块业务的一系列的表迁移到另外一个库是必然需要的。

    +----------------------------------------------+
A   |                                              |
    +----------------------------------------------+

    ++
B   ||
    ++

双写

    +----------------------------------------------+------------+
A   |                                              |            |
    +----------------------------------------------+------------+

    ++                                             +------------+
B   ||                                             |            |
    ++                                             +------------+

数据 dump

可以一次dump 10条数据 (拍脑袋),对于更新比较频繁的表可以适当减少,这里会有少量的数据是不一致的 (表的并发更新之类,dump 不能保证原子性),不用担心,我们可以在后面校验数据的时候直接修复掉

对 id取模,多进程dump 加快速度

    +----------------------------------------------+------------+
A   |                                              |            |
    +----------------------------------------------+------------+

    +-------------+                                +------------+
B   |             |                                |            |
    +-------------+                                +------------+

数据校验

这块要一条一条数据的进行对比 (扫表)

切读

在业务代码里面,将 db 的连接改到新库 B 上

去写

到这一步,代码里面的双写就可以直接去掉了;完美迁库。(旧库中的表不急忙删除,你懂的)

私信的设计

  1. 发件人和消息的关系 (一对多)

  2. 消息和收件人的关系 (多对多)

pre_messages

+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(11) unsigned | NO   | PRI | NULL    | auto_increment |
| sender_id  | int(11) unsigned | NO   |     | 0       |                |
| message    | varchar(255)     | NO   |     |         |                |
| type       | varchar(50)      | NO   |     |         |                |
| expires_at | datetime         | YES  |     | NULL    |                |
| created_at | datetime         | NO   |     | NULL    |                |
| updated_at | datetime         | NO   |     | NULL    |                |
| deleted_at | datetime         | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+

pre_message_recipients

+--------------+---------------------+------+-----+---------+----------------+
| Field        | Type                | Null | Key | Default | Extra          |
+--------------+---------------------+------+-----+---------+----------------+
| id           | int(11) unsigned    | NO   | PRI | NULL    | auto_increment |
| recipient_id | int(11) unsigned    | NO   |     | 0       |                |
| message_id   | int(11) unsigned    | NO   |     | 0       |                |
| status       | tinyint(1) unsigned | NO   |     | 0       |                |
| created_at   | datetime            | NO   |     | NULL    |                |
| updated_at   | datetime            | NO   |     | NULL    |                |
| deleted_at   | datetime            | YES  |     | NULL    |                |
+--------------+---------------------+------+-----+---------+----------------+

php中通配符的实现

该方法摘自Laravel5的Event getWildcardListeners

作用如下:

`item.*` -> `item.new`
         -> `item.edit`
class Str {
    public static function is($pattern, $value)
    {
        if ($pattern == $value) return true;

        $pattern = preg_quote($pattern, '#');
        $pattern = str_replace('\*', '.*', $pattern) . '\z';
        return (bool) preg_match('#^' . $pattern . '#', $value);
    }
}

该功能使用正则实现

  • * 替换成 .* 匹配所有字符

  • \z 这个转义序列从来没用过,查了手册是说不受修正符的影响,其实这边应该和$是一样的

PHP: Escape sequences - Manual