小子 发布的文章

在JAVA中实现PHP的gzuncompress

背景

在系统改造的时候,从php迁移到java;由于php中为了节省redis的内存,对缓存的数据做了 gzcompress 处理;为了能读取出数据,有两套方案:

  1. 刷数据,将redis中老的数据清理掉,去掉 gzcompress 的步骤(缺点:刷数据的时间,和读取代码上线的时间点无法吻合;数据的写入入口比较多,容易遗漏)

  2. java中读取的时候可以进行 gzuncompress

一些知识

知道这些知识候就能避免我在实现过程中遇到的很多问题。

PHP中的 gzcompress

This function compresses the given string using the ZLIB data format.
Note:
This is not the same as gzip compression, which includes some header data. See gzencode() for gzip compression.

一直以为 gzcompress 就是 gz 的压缩,php中使用的 zlib 来压缩,压缩完的结果中携带了头信息,直接使用 gz 解压是不认这种格式的。

JAVA中的 new String(byte[])

java.lang.StringCoding.StringDecoder 当在编码 byte[] 不能处理的时候会进行一些处理;所以说 (new String(compressedByte)).getBytes()compressedByte 并不一定会完全一样。

说到这里就可以看下 jedis 提供的接口了,刚开始我是使用的 String get(String key),于是由于上面的原因,当我用这个返回值 getBytes() 的时候就已经发生了变化。正确的使用方法应该是使用 byte[] get(byte[] key),由于比较繁琐,封装一下。

实现

    public static String get(Jedis jedis, String key) {
        byte[] byteKey = key.getBytes();
        byte[] element = jedis.get(byteKey);
        return new String(gzuncompress(element));
    }

    public static List<String> mget(Jedis jedis, List<String> keys) {
        byte[][] byteKeys = new byte[keys.size()][];
        for (int i = 0; i < keys.size(); i++) {
            byteKeys[i] = keys.get(i).getBytes();
        }
        List<byte[]> elements = jedis.mget(byteKeys);
        List<String> result = new ArrayList<>();
        for (byte[] element : elements) {
            result.add(new String(gzuncompress(element)));
        }
        return result;
    }

    public static byte[] gzuncompress(byte[] data) {
        byte[] unCompressed = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
        Inflater deCompressor = new Inflater();
        try {
            deCompressor.setInput(data);
            final byte[] buf = new byte[1024];
            while (!deCompressor.finished()) {
                int count = deCompressor.inflate(buf);
                bos.write(buf, 0, count);
            }
            unCompressed = bos.toByteArray();
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            deCompressor.end();
        }

        return unCompressed;
    }

记一次内存泄露的debug过程

在压测 代码在线运行 工具的时候,发现当并发比较高的时候程序占用的内存会飙升,而且在中断压测之后,内存占用并没有回落。

第一个能想到的办法就是去看代码,但是大多数时候,自己写的代码,很难review出太多的问题;于是就借助golang的pprof来定位问题。

在程序中嵌入 pprof

package main

import (
    "tool.lu/sandbox-server/app"
    "net/http"
    _ "net/http/pprof"
    "strconv"
    "runtime"
)

func main() {
    debug()
    server := app.NewApp()
    server.Run(":9090")
}

func debug() {
    go func() {
        // 这边是由于通过pprof发现问题之后,加的一段debug代码;后面会讲到
        http.HandleFunc("/go", func(w http.ResponseWriter, r *http.Request) {
            num := strconv.FormatInt(int64(runtime.NumGoroutine()), 10)
            w.Write([]byte(num))
        })
        http.ListenAndServe("localhost:6060", nil)
    }()
}

通过 go tool 工具,查看内存分配最多的 top 5

go tool pprof http://localhost:6060/debug/pprof/heap
top 5

Screenshot 2016-02-07 at 17.55.39.png

查看代码,发现是 goroutine, ioPipe 的问题,一定是使用姿势出了问题:

Screenshot 2016-02-07 at 17.56.40.png

于是便有了上面的那段代码,curl http://localhost:6060/go,查看当前 go routine 的数量;于是猜测是因为 ioPipe 没有正确的关闭,引起 go routine 大量的产生,但是没有退出,耗费大量的内存;于是在异常退出前,主动关闭 ioPipe 的Reader,至此问题解决。

压测验证

本机

wrk -t5 -c20 -d10000s -s post.lua http://tool.lu

服务器

Screenshot 2016-02-07 at 16.24.24.png

curl http://localhost:6060/go

总结

这是一个很小的bug,由于写代码的时候不仔细,return之前没有关闭资源造成,但却要花费不少的力气去解决;对语言自己提供的工具链需要熟悉在熟悉,这样不管在解决问题或者避免问题的时候,都能节省很多的时间。

JAVA WEB乱码解决

设置环境变量

export $LANG="UTF-8"

设置tomcat接收GET参数时候的编码 server.xml (tomcat中的配置文件)

<Connector
    ....
    URIEncoding="UTF-8">

设置接收POST参数时候的编码

web工程中web.xml的设置

必须要放在所有filter之前

    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

使用CLion开发PHP扩展

首先使用php源码中的命令生成一个扩展结构

./ext_skel --extname=hank
cd hank
phpize
./configure

创建CMakeLists.txt

由于CLion目前还不支持MakeFile文件,所以要使用CMakeLists.txt进行中转

cmake_minimum_required(VERSION 3.3)
project(hank)

add_custom_target(makefile COMMAND make && make install
        WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})

设置编译环境和调试脚本

QQ20151106-0.png

至此基本全部搞定,可以直接下断点,以debug模式进行调试

对于读多写少的少量数据的缓存优化

前提:数据量比较少,并且读多写少,实时性要求不高的数据。

优点

  1. 访问本地cache节省了网络开销,减少中心cache集群(redis)的压力

  2. 正常的web机器内存利用率较低,减少web机内存资源的浪费

类库的选择

google guava cache

  1. LRU失效机制

  2. 流畅的api接口

  3. 使用内存缓存

  4. 配套stats接口

流程

未命名文件.png

  1. [被动失效] controller层访问cache,若获取不到数据,从db中获取数据,并刷到cache

  2. [主动失效] 当db数据变更时,主动失效缓存;注意:这里cache是在每台web机器都有一份,所以每台机器都需要刷一遍;所以我们需要一个配置中心;在web机器监听配置中心的变化,然后刷新各自机器的cache

  3. 定时将本机的cache的 命中率,量 上报到监控系统

主动失效机制

  1. 实时性要求较高的可以订阅mysql的binlog

  2. 实时性要求不高的可以定时跑crontab

casperjs兼容reactjs截图

由于phantomjs使用的webkit内核版本较低,不支持es5;自然phantomjs无法正常打开reactjs的页面;这里可以使用es5-shim.js 来兼容。

casperjs是可以使用clientScripts将 es5-shim.js 插入页面中;但是这个插入是在页面的加载完之后;我们需要的是在页面的最前面插入 es5-shim.js;于是:

casper.options.onPageInitialized = function() {
    casper.page.injectJs("es5-shim.js");
};

[开源] kaka 咔咔

项目地址:https://github.com/xiaozi/kaka
下载地址:https://github.com/xiaozi/kaka/releases

依赖

  1. nsq

  2. casperjs

  3. phantomjs

安装

  1. 将 .env.example 拷贝为 .env

  2. 修改 .env, 填写信息

  3. 运行

./kaka

用法

只需要将消息塞到 nsq 的 topic 中就可以了,topic 是你在 .env 文件里面设置的

消息使用 json 格式,结构如下:

{
    "url": "http://tool.lu/",
    "target": "/data/screenshots/WrTSV5zbkHPCqU6t.png",
    "path": "screenshots/WrTSV5zbkHPCqU6t.png",
    "device": "mac"
}

url: (必须) 需要截图的url

target: (必须) 截图在服务器上的保存的绝对路径

path: (可选) 上传到七牛云的路径,不填则不上传

device: (可选) 目前只支持 “mac" 这个值

FAQ

  1. 使用casperjs截出优雅的图片

  2. 多种网络环境的处理

在每台机器上都配置一个kaka,然后让他们从不同channel的订阅

  1. 不想上传到七牛云

消息中的path留空就好了

开发依赖

go get -u github.com/joho/godotenv
go get -u github.com/qiniu/api.v7
go get -u github.com/bitly/go-nsq

Spring RequestParam过于智能

POST到后端的数据是这样的

keywords[]: 关键词1,关键词2
keywords[]: 关键词3,关键词4
@RequestParam(value = "keywords[]") List<String> keywords;

然而经过Spring的RequestParam处理之后,却变成了 ["关键词1", "关键词2", "关键词3", "关键词4"];对于 Spring 这种过分聪明的行为我表示很呵呵~

防止这种解析的办法就是覆盖掉原有的converters;不使用英文逗号为分隔符。

<mvc:annotation-driven conversion-service="conversionService"/>

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="lu.tool.util.StringToArrayConverter" />
        </set>
    </property>
</bean>
package lu.tool.console.util;

import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.List;

/**
 * Created by xiaozi on 8/12/15.
 */
public class StringToArrayConverter implements Converter<String, List<String>>{
    @Override
    public List<String> convert(String source) {
        return Arrays.asList(StringUtils.delimitedListToStringArray(source, ";"));
    }
}

fastjson处理Map的 key 为 int 的bug

JSON 中的 MAP 是不支持 int 为 key 的;

Map<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
System.out.println(JSON.toJSONString(map));
// {1:"one",2:"two"}

标准的json规范中,要求对象的key必须为string

php扩展实践zend_execute_ex层获取实参

其实在实现的 php 函数里面是很容易获取到的,参考 php 的 builtin 函数 func_get_args() 就可以知道了。

void **p;
int arg_count;
int i;
zend_execute_data *ex = EG(current_execute_data);

if (!ex || !ex->function_state.arguments) {
    RETURN_FALSE;
}

p = ex->function_state.arguments;
arg_count = (int)(zend_uintptr_t) *p;

for (i = 0; i < arg_count; i++) {
    zval *element, *arg;
    arg = *((zval **) (p - (arg_count - i)));
    php_var_dump(&arg, 1 TSRMLS_CC);
}

但是在 zend_execute_ex 中,是不能使用 function_state.arguments 来获取参数的,需要从 argument_stack 中获取调用函数的实参。

static void (*old_zend_execute_ex) (zend_execute_data *execute_data TSRMLS_DC);

ZEND_API void learn_execute_ex (zend_execute_data *execute_data TSRMLS_DC)
{
    php_printf("====== extension debug start ======\n");
    php_printf("function name: %s\n", get_active_function_name(TSRMLS_C));

    old_zend_execute_ex(execute_data TSRMLS_CC);

    int stacked = 0;
    void **top;
    void **bottom;
    zval *arguments;
    smart_str buf = {0};

    array_init(arguments);
    
    top = zend_vm_stack_top(TSRMLS_C) - 1;
    if (top) {
        stacked = (int)(zend_uintptr_t) *top; // argc
        if (stacked) {
            bottom = zend_vm_stack_top(TSRMLS_C);
            EG(argument_stack)->top = top + 1;
            if (zend_copy_parameters_array(stacked, arguments TSRMLS_CC) == SUCCESS) {
                php_json_encode(&buf, arguments, 0 TSRMLS_CC);
            }
            EG(argument_stack)->top = bottom;
        }
    }

    smart_str_0(&buf);

    php_printf("%s\n", buf.c);

    smart_str_free(&buf);
    zval_dtor(arguments);

    php_printf("====== extension debug end ======\n");
}

PHP_MINIT_FUNCTION(learn)
{
    old_zend_execute_ex = zend_execute_ex;
    zend_execute_ex = learn_execute_ex;

    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(learn)
{
    zend_execute_ex = old_zend_execute_ex;

    return SUCCESS;
}

2015-11-04 00:38 更新

后来看到,其实不用上面这中方法就可以实现, php 5.5之后要从 prev 里面去取

/**
 * php_var_dump defined in this head file.
 */
#include "ext/standard/php_var.h"

zend_execute_data *real_execute_data = execute_data->prev_execute_data;

void **p = real_execute_data->function_state.arguments;
int arg_count = (int) (zend_uintptr_t) * p;
zval *argument_element;
int i;
// zval *obj = real_execute_data->object;
unsigned long start = mach_absolute_time();
for (i = 0; i < arg_count; i++) {
    argument_element = *(p - (arg_count - i));
    php_var_dump(&argument_element, 1);
}

php max_input_vars限制

这是一次线上 bug,后台 sku 修改的时候,使用了大量的数组,造成了 php 端 $stock 变量无法完全解析。

stock[1][price]=1234
stock[1][stock]=99
stock[1][id]=1
// ...
stock[500][price]=1234
stock[500][stock]=99
stock[500][id]=500

由于线上的 php 版本是由低版本升级上去的,所以以前是不会有这样的问题的,PHP 5.3.9为了防止hash冲突就加了那么个参数 max_input_vars

曾今天真的以为上面的例子的 input vars 应该是 500 个,看了 PHP 的源码之后才发现应该是 500 * 3;其实 PHP 是按照 & 为 token 来计数的。

// vim main/php_variables.c +438
// 这里的 separator,在 GET POST 中是 &; 在 COOKIE 中是 ;
var = php_strtok_r(res, separator, &strtok_buf);

// :458
while (var) {
    if (++count > PG(max_input_vars)) {
        // ...
    }
}

如何避免这样的问题

  1. 自然是想办法增加 max_input_vars 的大小,但这样有个难以逃避的问题,就是前端传过来数据的个数你是没办法知道应该是多少的

  2. 减少 post 的字段,先 js json_encode 一下,再用一个大字段传过来

  3. 使用 post 的 body 将数据传过来;在 php 端使用 file_get_contents('php://input') 来获取之后在 json_decode;我觉得这中方法比较可取,因为 restful 设计里面就采用的这种方法。

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);
    }
}