新项目代码规范

Result类

结构

{
  "code": 0,
  "msg": "ok",
  "data": null
}
  • code:为错误编码,只能是int
  • msg:为错误信息,只能是字符串
  • data:为数据,可以是任何类型

code的定义

说明
0成功
大于0为业务错误,可直接将msg展示给前端
小于0为系统错误,不应将msg展示给前端

性别

  • 字段名:gender
说明
0未知
1
2

版本号

字符串格式

<major>.<minor>.<patch> 或 v<major>.<minor>.<patch>

三段必须都有,不能省略,如:v1.0.0,v1.2.3,2.5.7

  • major为大版本,升级major时可不向下兼容
  • minor为功能版本,升级minor时应尽可能向下兼容
  • patch为补丁版本,升级patch时必须向下兼容,升级patch版本时不应提供新功能或新接口

数字格式

正整数,最后三位代表patch,中间三位代表minor,最前面的若干位代表major

如:1000000为v1.0.0,21001134代表v21.1.134

在mac中通过shell弹窗或发通知

Mac中有个AppleScript,通过它可以实现弹窗或系统通知

弹窗

osascript -e 'display alert "告警!!" as critical'
  • osascript -e 是执行一段apple script
  • display alert是弹窗
  • as critcal是用来控制图标,可以as三个值(informational,warning,critical,默认是informational)

发通知

osascript -e 'display notification "通知内容" with title "标题" subtitle "子标题" sound name "Glass"'
  • display notification 是发通知的命令后面紧跟着通知内容
  • with title 可以修改通知标题(默认是“脚本编辑器”)
  • subtitle 可以添加一个子标题(默认没有)
  • sound name 可以用来选择一种通知的声音,声音存放在/System/Library/Sounds

结合PHP实战

<?php
ini_set('date.timezone','PRC');

$host = 'mysql.example.org';
$port = '3306';
$dbname = 'test';
$username = 'root';
$password = '123456';
$pdo = new PDO("mysql:host={$host};port={$port};dbname={$dbname};charset=UTF8", $username, $password);

$sql = <<<SQL
SELECT u.uid, p.privilege_id, u.updated_at 
FROM user_privilege AS p 
LEFT JOIN user AS u ON u.uid = p.uid
WHERE u.del = 1 and p.del = 0;
SQL;

$statement = $pdo->query($sql);
$result = $statement->fetchAll(PDO::FETCH_ASSOC);
$new = [];

$logDir = __DIR__ . '/log/';
if (!file_exists($logDir)) {
    mkdir($logDir, 0755, true);
}
foreach ($result as $item) {
    $fileName = $logDir . '/' . $item['uid'] . '.json';
    if (!file_exists($fileName)) {
        file_put_contents($fileName, json_encode($item, JSON_PRETTY_PRINT));
        $new[] = $item['uid'];
    }
}
if ($new) {
    $msg = sprintf('%s发现新异常数据%d条!(%s)', date('Y-m-d H:i:s'), count($new), implode(', ', $new));
    $cmd = <<<CMD
osascript -e 'tell application "System Events" to display alert "{$msg}" as critical'
CMD;
    exec($cmd);
}

这个脚本连接一个远程的数据库,检查是否出现用户被删除,但是权限没有被删除的情况,如果发现将它记录到日志并弹窗提示。这里增加了tell application "System Events" to这么个前缀,是指以“System Events”这个应用弹这个窗,这是因为好像有什么机制会阻止cron任务中直接弹窗,需要借用另一个应用的身份。也可以借用”Finder”,发弹窗时Finder会跳动一下,但是弹窗需要点Finder才能出来,而使用System Event可以直接弹出来。

接下来可以在执行crontab -e来增加个计划任务

MAILTO=""
*/1 * * * * php ~/check.php > /dev/null
  • MAILTO=”” 是执行crontab不发执行结果的系统邮件
  • */1是指每1分钟执行一次,剩下的*分别代表:每小时(0~23)/每天(1~31)/每月(1~12)/每周几(0~6)

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 [email protected]"),"\n";  //this_is_a_test_text

echo StringCase::kebabCase("This_is [email protected]"),"\n";  //this-is-a-test-text

echo StringCase::pascalCase("This_is [email protected]"),"\n"; //ThisIsATestText

echo StringCase::camelCase("This_is [email protected]"),"\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等发方法的第一行