StringBuilder和StringBuffer

StringBuilder线程不安全。

StringBuffer线程安全。

如果多线程同时构建一个字符串,应该使用StringBuffer,但是什么时候会多线程去构建同一个字符串呢?

命名格式转换

在做一些生成代码或生成配置时经常会遇到命名格式不一样的问题,比如有的地方要用驼峰命名(比如代码的类和遍历),有的地方要用下划线命名(比如数据库字段)。因此写了个小工具来快速将一组单词转换成特定格式的命名。

因为我的使用场景中没有太严苛的性能要求,所以里面用到了正则,并且应该也不是最优解,所以不建议在高并发的场景用(也没做过性能测试)。

在查规范的时候学习到了一些有意思的单词,很形象的形容了命名格式。
比如下划线命名是snake,纯大写的下划线命名(比如常量定义AAA_BBB_CCC)是screaming snake(尖叫蛇)。
大驼峰是pascal(这个其实不太理解为什么是帕斯卡)。
用横线命名(比如url、域名、docker服务名:aaa-bbb-ccc)是kebab(烤肉串)。

<?php
/**
 * Created by PhpStorm.
 * User: fyn
 * Date: 2018/10/9
 * Time: 10:40 AM
 */
namespace Fyn\Common;

class StringCase
{


    /**
     * snake_case
     * @param $string
     * @return string
     */
    public static function snakeCase($string) {
        $string = lcfirst($string);
        $string = preg_replace_callback('/[A-Z]+/', function ($text) {
            return '_'.strtolower($text[0]);
        },$string);
        $string = preg_replace('/[^a-zA-Z0-9]+/','_',$string);
        return $string;
    }

    /**
     * SCREAMING_SNAKE_CASE
     * @param $string
     * @return string
     */
    public static function screamingSnakeCase($string) {
        return strtoupper(self::snakeCase($string));
    }

    /**
     * kebab-case
     * @param $string
     * @return string
     */
    public static function kebabCase($string) {
        return str_replace('_','-',self::snakeCase($string));
    }

    /**
     * PascalCase
     * @param $string
     * @return string
     */
    public static function pascalCase($string) {
        $arr = preg_split('/[^a-zA-Z0-9]/', $string);
        $string = implode('',array_map('ucfirst',$arr));
        return $string;
    }

    /**
     * camelCase
     * @param $string
     * @return string
     */
    public static function camelCase($string) {
        return lcfirst(self::pascalCase($string));
    }

    /**
     * htmlcase
     * @param $string
     * @return string
     */
    public static function htmlCase($string) {
        return strtolower(preg_replace('/[^a-zA-Z0-9]+/','', $string));
    }
}

使用方法:

use Fyn\Common\StringCase;

echo StringCase::snakeCase("This_is A-test@text"),"\n";  //this_is_a_test_text

echo StringCase::kebabCase("This_is A-test@text"),"\n";  //this-is-a-test-text

echo StringCase::pascalCase("This_is A-test@text"),"\n"; //ThisIsATestText

echo StringCase::camelCase("This_is A-test@text"),"\n";  //thisIsATestText

缓存穿透,缓存雪崩,缓存击穿

缓存穿透

问题:当访问一个不存在的商品id时因为没有缓存会直接查数据库,如果有高并发访问不存在的id可能导致数据库压力变大。

解决方法:可以把空值也进行缓存,但缓存时间可以相对较低

缓存雪崩

问题:如果大量商品的缓存是在同一时间生成的,比如每天零点跑脚本更新缓存,会导致1点时大量缓存同时过期,这时会出现大量数据库查询。

解决方法:可以将过期时间加上一些随机因子,比如过期时间随机设置为为45分钟至1小时15分。

缓存击穿

问题:缓存未生成或过期后,突然高并发访问。因为生成缓存需要一定的时间,所以在这段时间并发请求都认为缓存不存在都去查数据库。

解决方法1:设置不过期的缓存(在修改数据时再更新缓存)

解决方法2:在缓存的数据中也记录一个时间,但比缓存的过期时间短。比如缓存1小时,数据中记半个小时。半小时后再访问这条数据时仍然从缓存中返回数据,但是同时会往消息队列中发一条请求更新缓存的消息。然后后台任务消费消息时去更新缓存,在更新缓存前要再次检查时间,因为同一个商品并发访问时有可能发送多条消息,但是实际上去只需要更新一次缓存就够了,剩下的可以忽略。这里还涉及到消息分区的问题,因为一般会多线程消费消息队列,这时要保证同一个商品的更新请求不会出现在多个线程中。

方法2的方案更复杂,而且只能缓解击穿,不能避免,唯一的优势就是缓存会过期,能节省一部分缓存服务的使用空间。

InnoDB 聚集索引和非聚集索引

区别

聚集索引包含所需的所有数据,比如mysql整张表就是一个聚集索引
比如索引的key就是主键,值就是这一行的所有字段,根据主键查询时可以直接查到所有字段

非聚集索引不包含所有数据,但会包含聚集索引的key
比如索引的key是两个自定义的字段,而值就是主键,根据这两个字段,只能直接查出主键是什么,所以这时如果想查询其它字段的值还会通过主键去聚集索引中再查询一次(即回表)。

覆盖索引减少回表

如果查询的条件或结果中,只使用了非聚集索引定义中的那几个字段的话,就不会回表二次查询。
比如索引是uid, username两个字段:
执行SELECT status WHERE uid = ?时会回表二次查询status是什么
但执行SELECT username WHERE uid = ?时就不会二次查询,因为username在索引定义中。

如何创建聚集索引

聚集索引只是存储方式,不是索引类型,所以在创建索引时无法指定是否是聚集索引。

默认情况下会根据主键创建聚集索引。
如果没有主键,会根据第一个唯一索引创建聚集索引。
如果也没有唯一索引,会使用row id做聚集索引。

延伸:MyISAM

MyISAM中即使是聚集索引节点中也不包含数据,而是数据在硬盘的物理地址。而非聚集索引同样存的也是物理地址,所以没有回表问题。这么来看MyISAM好像比InnoDB快。但是因为MyISAM的节点上没有存储数据,所以数据在磁盘上有可能不是连续顺序存储的(按插入时间写入),那么在按范围查询的时候是不是会增加磁盘读取次数。

B树和B+树的区别

B树中的元素不会出现重复的,元素有可能在中间节点也有可能在叶子节点。
B+树中所有元素都会出现在叶子节点,但也有可能同时出现在中间节点。B+树叶子节点间有额外的链表结构。
这两点会让B+树在按连续的范围查找时比B树更快。
另外B树在查找时不同的元素因为在不同层级的节点,所以不同的元素查询时间可能有差异,而B+树所有的元素都在叶子节点所以查询的时间更平均一些

B树每个元素都包含卫星数据。
B+树只有叶子节点中的元素包含卫星数据。
这会使在中间节点的页大小相同的情况下B+树能比B树存更多的节点,也就意味着B+树有可能比B树更矮一些(树的高度决定磁盘io次数,越矮次数越少)

logback设置自定义字段

配置文件

logback_spring.xml

<appender name="myAppender"  class="ch.qos.logback.core.ConsoleAppender">
    <encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder">
        <includeCallerInfo>false</includeCallerInfo>
        <includeMdc>false</includeMdc>
        <includeContext>false</includeContext>
        <fieldNames>
            <timestamp>time</timestamp>
            <message>info</message>
            <version>[ignore]</version>
            <logger>[ignore]</logger>
            <thread>[ignore]</thread>
            <levelValue>[ignore]</levelValue>
        </fieldNames>
    </encoder>
</appender>
<logger name="pw.fyn.test" additivity="false">
    <appender-ref ref="myAppender"/>
</logger>
  • encoder
    • includeCallerInfo:是否包含调用信息
    • includeMdc:是否包含mdc信息,如果填true,则每一个字段都会在输出的json中出现
    • includeContext:是否包含上下分,如果填true,每一个<springProperty scope="context" />都会输在json中
    • fieldNames:默认字段的映射关系,比如把@timestamp改为了time,忽略掉versionthread等四个字段
  • logger
    • name:受影响的包或类
    • additivity:填false后将不会再输出到默认的root中
    • appender-ref:关联appender

写日志的代码

Printer.java

package pw.fyn.test;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import net.logstash.logback.marker.Markers;
import org.slf4j.Marker;


@Slf4j
public class Printer {

    public void print() {
        LogData data = new LogData();
        Marker marker = Markers.append("data", data);
        log.info(marker, "test info");
    }

    @Data
    private static class LogData {
        private int version = 3;
        private String topic = "order.create";
        private String message_id = "00000000-0000-0000-0000-000000000000";
    }

}

需要额外增加的字段需要通过Marker的方式放到info、error等发方法的第一行

php禁用xdebug的彩色html var_dump

这个功能有时会麻烦,直接看请求结果很乱,而且会忽略掉一些大的字段,可以通过这哥参数禁用

ini_set('xdebug.overload_var_dump', 0);
ini_set('html_errors', 0);

用PHP做gRPC的服务端

gRPC的官方文档中虽然没有给出PHP做Server的例子,但实际上grpc的扩展是支持的。

参考扩展中提供的几个类以及扩展内部的一些代码,写了一个简单的示例

这个示例只是证明PHP可以直接写gRPC服务,不代表可以在生产环境中这么用。主要原因有这么几点:

  1. 不借助pcntl或swoole的话是个单进程的服务,并发会阻塞
  2. 没做性能测试以及是否有内存泄漏的测试
  3. 而且直接PHP做Server的争议也比较大

完整示例代码(https://github.com/ssfyn/php-grpc-server-example

Server端的主要逻辑:

$this->server = new \Grpc\Server([]);
$this->server->addHttp2Port('0.0.0.0:50051');
$this->server->start();

while($request = $this->server->requestCall()){
    $method = $request->method;
    $call = $request->call;
    if ($method=='/dev.fyn.HelloWorld/SayHello') {
        //接收请求
        $recv = $call->startBatch([
            \Grpc\OP_RECV_MESSAGE => true
        ]);
        //将请求的数据转换为对象
        $request = new SayHelloRequest();
        $request->mergeFromString($recv->message);
        //调用业务代码
        $impl = new HelloWorldImpl();
        $response = $impl->sayHello($request);
        //处理返回值,即使没有metadata也要设置OP_SEND_INITIAL_METADATA,不然会一直阻塞在这里
        $call->startBatch([
            \Grpc\OP_SEND_INITIAL_METADATA => [],
            \Grpc\OP_SEND_MESSAGE => [
                'message'=>$response->serializeToString()
            ],
            \Grpc\OP_SEND_STATUS_FROM_SERVER => [
                'code'=>\Grpc\STATUS_OK,
                'details'=>'OK'
            ],
        ]);
    } else {
        $call->startBatch([
            \Grpc\OP_SEND_INITIAL_METADATA => [],
            \Grpc\OP_SEND_STATUS_FROM_SERVER => ['code'=>\Grpc\STATUS_NOT_FOUND,'details'=>'Not found'],
        ]);
    }
}

Mac安装PHP+GRPC

安装xcode命令行工具

xcode-select --install

安装brew

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

通过brew安装php5.6(或其它版本)

brew install php@5.6

通过brew安装php切换器

brew install brew-php-switcher

切换php版本

brew-php-switcher 5.6

通过pecl安装PHP的protobuf扩展(要编译,比较慢)

pecl install protobuf

通过pecl安装PHP的grpc扩展(要编译,比较慢)

pecl install grpc