Mysql 锁的介绍和使用

参考

基本概念

并发

网络编程中,多个请求/查询(进程/线程) 修改同一数据时,就会产并发控制问题,即谁先修改谁后修改.锁机制就是用来解决和控制并发问题的.

Mysql根据加锁的范围(粒度)分为三种锁

全局锁

给整个数据库加锁

FLUSH TABLES WITH READ LOCK
//全局读锁,会使其它线程中的写操作 数据更新语句(删除更新) 数据定义语句(创建修改表) 和更新事物提交类语句被阻塞
//使用场景全库逻辑备份,存在无事物的存储引擎的数据库
mysqldump –single-transaction 适合全库innodb引擎(所有表都支持事物)备份

表级锁

一种是表锁,一种是元数据锁(meta data lock,MDL)

表锁 lock tables tablename… read/write 释放所unlock tables

表锁不仅仅会锁定其它线程对锁定表的操作,也会锁定当前执行lock 语句的线程接下里的操作对象.只能执行当前表的操作

当前线程锁定users表,无法操作其它表
其它线程可以正常读取users表

上图示例当前线程执行了所锁定users表,则无法查询或操作其它表,

此时其它线程中可以正常读取users表的数据,因为读锁不互斥.

锁定执行写操作
释放表锁
另一线程内的查询操作

上三图表示线程A锁定users表示,其它线程对users表的操作,在未释放表锁前,另一线程一直处于阻塞状态,直到释放表锁

另一类表级的锁是MDL(meta data lock)

不需要显示调用,数据库自动操作 Mysql5.5版本中开始引入.作用是保证读写的正确性.例如,一个线程进行遍历查询,另一个线程进行表结构变更.

对一个表进行增删改查操作时,加MDL读锁,对表执行结构变更时,加MDL写锁.

读锁之间不互斥,因此多个线程可以对同一张表做增删改查,

读写锁之间,读锁之间互斥,用户保证表结构变更操作的安全性.例如线程A进行表遍历查询,线程B进行表结构变更操作,线程B需要等线程A的读锁释放之后才能执行

MDL测试案例

测试数据20万
测试代码,事务中遍历数据表
sessionA执行代码中的数据表遍历
sessionB执行Alter语句修改users表结构

此时,sessionB会阻塞,因为sessionA中的MDL读锁还没有被释放

sessoinC中的所有查询都处于等待状态

sessionC所有查询被被阻塞,此时整个数据库不能正常查询,这就是生产环境数据库结构变更可能会踩的坑.

解决方案

1.解除长事务,变更前查询一下存在事物正在运行的事物(information_schema 库的 innodb_trx表),手动kill调.

2.alter table语句设定等待时间. MariaDB和AliSQL支持 DDL NOWWAIT?WAIT n 语法

ALTER TABLE tbl_name NOWAIT add column...

ALTER TABLE tbl_name WAIT n add column...

MDL总结

mysql 5.6之前的版本,执行alter语句时间会很长,通过建立新的临时表方案解决,导入数据,最后重命名.

mysql5.6引入online ddl,通常情况下不会锁库,但是存在长事务和慢查询时,也会出现

行锁

行锁是由存储引擎层实现的,是mysql最小锁定粒度,可以锁定一行数据.常用引擎Innodb支持行锁MyISAM不支持行锁

Innodb行锁介绍

两阶段加锁,,

Laravel 迁移文件 简单总结

参考

简介

数据库迁移就像是数据库的版本控制,可以让你的团队轻松修改并共享应用程序的数据库结构。迁移通常与 Laravel 的数据库结构生成器配合使用,让你轻松地构建数据库结构。如果你曾经试过让同事手动在数据库结构中添加字段,那么数据库迁移可以让你不再需要做这样的事情。

执行 php artisan migtate 后 数据库中会生成一个迁移文件表 migrations ,每一条记录对应一个执行过的迁移文件,怎么看每次迁移了哪些文件?在 migrations 表中有一个 batch 字段,字段值相同的为同一次迁移

创建 created方法

Schema::create('users', function (Blueprint $table) {
    //...		
});

修改 table方法

数据库因为业务需要变更时,每个表的变更创建一个单独的迁移文件方便生产执行.

需要引入composer require doctrine/dbal 扩展包

Schema::table('migration_demo', function (Blueprint $table) {

});

1 对一个字段做多种修改 例如 重命名和修改类型同时进行.原字段为type 类型int

//无效方式1
Schema::table('migration_demo', function (Blueprint $table) {
    $table->bigInteger('type')->default('0')->change();
    $table->renameColumn('type', 'demo_type');
    //经测试这两号代码颠倒顺序最后生成的语句是一样的
});

执行语句, 字段重命名时又改回了默认的int类型

ALTER TABLE migration_demo CHANGE type demo_type INT DEFAULT 0 NOT NULL
ALTER TABLE migration_demo CHANGE type type BIGINT DEFAULT 0 NOT NULL
//无效方式2
Schema::table('migration_demo', function (Blueprint $table) {
    $table->bigInteger('type')->default('0')->change()->renameColumn('type', 'demo_type');
});

执行语句 ,rename并没有生效

ALTER TABLE migration_demo CHANGE type type BIGINT DEFAULT 0 NOT NULL

同一字段执行多种变更,正确的方式

Schema::table('demo', function (Blueprint $table) {
   $table->renameColumn('name', 'demo_name');
});
Schema::table('demo', function (Blueprint $table) {
   $table->string('demo_name', 255)->default('')->change();
});

执行语句结果

ALTER TABLE demo CHANGE demo_name demo_name VARCHAR(255) DEFAULT '' NOT NULL COLLATE utf8mb4_unicode_ci	
ALTER TABLE demo CHANGE name demo_name VARCHAR(20) DEFAULT '' NOT NULL

迁移文件的另一种简介执行方式,使用DB statement 执行原生DDL语句

DB::statement("ALTER TABLE `lara`.`users` CHANGE COLUMN `remember_token` `remember_tokens` VARCHAR(255) COLLATE 'utf8mb4_unicode_ci' NULL DEFAULT NULL");

总结

个人感觉这种方式适合中小型项目和公司,在有DBA的公司应该使用专业的数据库迁移工具

Laravel Telescope

参考

简介

Larave Telescope 是 Laravel 框架的官方出品的debug工具包, 5.7.7以上版本才有.

此文用来记录一些文档中没有介绍的坑点,使用技巧以及原理

安装配置参考文档

1.扩展自带数据表迁移文件,配合其它迁移文件扩展包生成时要注意

定制数据迁移

如果您不打算使用 Telescope 的默认迁移,则应该在 AppServiceProvider 的 register 方法中调用 Telescope::ignoreMigrations 方法。您可以使用 php artisan vendor:publish --tag=telescope-migrations 命令导出默认迁移。

注意:如果使用三方扩展包生成迁移文件或手动编写迁移文件时不要生成Telescope相关迁移文件,否则执行时会造成冲突

按钮功能介绍

1暂停 2刷新 3定制Tag 可以对指定Tag数据进行监控

Tag设置规则,Auth:ID 模型:ID 可以看一下 telescope_entries_tags 表中自动记录的标签。但是在环境变量设置为production后,除了Auth其它的并不好使

生产环境通过认证访问

protected function gate()
    {
        Gate::define('viewTelescope', function ($user) {
            return in_array($user->email, [
                'jordon.kub@example.com', 'njerde@example.net'
            ]);
        });
    }

设置制定邮箱或自定以其他字段的用户,前提必须使用laravel自带的用户认证功能

测试问题

参考

启用了telescope(望远镜)debug工具之后,用phpunit 跑所有测试是会报ReflectionException: Class env does not exist 错误

解决方案

You could just add <env name="TELESCOPE_ENABLED" value="false"/> to the phpunit.xml-file.
//将TELESCOPE_ENABLED false添加到phpunit.xml配置中

Restful Api 风格规范研究

状态码用http status code 还是 在response body 中自定义code

列举总结一下各大厂的API的设计风格

总结

  • 国内资讯类大厂出于兼容性考虑 以及杀毒软件和安全浏览器 拦截404 页面 等并未采用REST API
  • 业务复杂度高的开放平台也未采用Rest API 如支付宝,微信,微博
  • 国外API多采用REST API 包括很多框架的设计(PHP Laravel)

百度手机站

jsonp http status code 200

百度PC站

jsonp ,收集搜索数据接口 http status code 200

今日头条PC

json,使用message区分状态 http status code 200

今日头条手机站

json http status code 200 has_more 区分

新浪微博PC

json http status code 200 直接返回html

微博手机站

Google手机站

restful api

极光推送

RestFul API HTTP STATUS CODE + Response Body 自定义CODE

GitHub

Restful API https://developer.github.com/v3/#client-errors

个人结论

遵守RFC规范,遵守业内标准,融入生态,工业技术是要标准化的。

更详细的论述看左耳朵耗子的文章 “一把梭: REST API 全用 POST”

应用案例

laravel框架默认的错误信息返回风格

去掉最外层data数据包裹 文档

参考

Linux网络调试 常用命令和分析工具 排查方法

查看/配置网络

ifconfig 查看IP地址

https://wangchujiang.com/linux-command/c/ifconfig.html

enx00e04cf0a10c: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.5  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 2408:8207:305c:1c30:b48f:56c7:81bb:bb9  prefixlen 64  scopeid 0x0<global>
        inet6 fe80::2c1b:1b85:35cd:9642  prefixlen 64  scopeid 0x20<link>
        inet6 2408:8207:305c:1c30:e49b:195a:eeaa:1e44  prefixlen 64  scopeid 0x0<global>
        ether 00:e0:4c:f0:a1:0c  txqueuelen 1000  (Ethernet)
        RX packets 29715  bytes 31484642 (31.4 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 22107  bytes 3794564 (3.7 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 4003  bytes 2920205 (2.9 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4003  bytes 2920205 (2.9 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
 
wlp4s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.0.111  netmask 255.255.255.0  broadcast 192.168.0.255
        inet6 fe80::6e92:514f:2d68:1da1  prefixlen 64  scopeid 0x20<link>
        ether 14:4f:8a:60:f2:2a  txqueuelen 1000  (Ethernet)
        RX packets 106  bytes 9074 (9.0 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 129  bytes 19965 (19.9 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

e开头的一般是有线网卡 例eth

lo 全称是 loopback,又称环回接口,往往会被分配到 127.0.0.1 这个地址。这个地址用于本机通信,经过内核处理后直接返回,不会在任何网络中出现

wlp开头表示无线网卡

flags = 4163 没有搜到什么意思

UP表示接口启用

BROADCAST 表示主机支持广播

RUNNING 表示接口正在工作

MULTICAST 主机支持多播

MTU:1500 最大传输单元 1500字节 百科

inet 网卡的IP地址

netmask 网络掩码

broadcast 广播地址

inet6 fe80::2aa:bbff:fecc:ddee prefixlen 64 scopeid 0x20<link> IPv6地址

ether 00:e0:4c:f0:a1:0c 连接类型:Ethernet (以太网) HWaddr (硬件mac地址)

txqueuelen (网卡设置的传送队列长度)

RX packets 接收时,正确的数据包数

RX bytes 接收的数据量

RX errors 接收时,产生错误的数据包数

RX overruns 接收时,由于速度过快而丢失的数据包数

RX dropped 接收时,丢弃的数据包数

RX frame 接收时,发生frame错误而丢失的数据包数

TX carrier 发送时,发生carrier错误而丢失的数据包数

TX collisions 冲突信息包的数目

mii-tool 配置网络设备协商方式的工具

https://wangchujiang.com/linux-command/c/mii-tool.html

是用于查看、管理介质的网络接口的状态,有时网卡需要配置协商方式,比如10/100/1000M的网卡半双工、全双工、自动协商的配置。但大多数的网络设备是不用我们来修改协商,因为大多数网络设置接入的时候,都采用自动协商来解决相互通信的问题。不过自动协商也不是万能的,有时也会出现错误,比如丢包率比较高,这时就要我们来指定网卡的协商方式。mii-tool就是能指定网卡的协商方式。下面我们说一说mii-tool的用法。

sudo mii-tool -v enx00e04cf0a10c
enx00e04cf0a10c: negotiated 100baseTx-FD, link ok
product info: vendor 00:07:32, model 0 rev 0
basic mode: autonegotiation enabled
basic status: autonegotiation complete, link ok
capabilities: 1000baseT-FD 100baseTx-FD 100baseTx-HD 10baseT-FD 10baseT-HD
advertising: 1000baseT-FD 100baseTx-FD 100baseTx-HD 10baseT-FD 10baseT-HD flow-control
link partner: 100baseTx-FD 100baseTx-HD 10baseT-FD 10baseT-HD

link ok 表示网线连接ok

basic mode: autonegotiation enabled 启用自动协商

route 查看网关

https://wangchujiang.com/linux-command/c/route.html

显示并设置Linux中静态路由表

常用方式 route -n 不执行DNS反向查找,直接显示数字形式的IP地址

route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG    100    0        0 enx00e04cf0a10c
0.0.0.0         192.168.0.1     0.0.0.0         UG    600    0        0 wlp4s0
169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 wlp4s0
192.168.0.0     0.0.0.0         255.255.255.0   U     600    0        0 wlp4s0
192.168.1.0     0.0.0.0         255.255.255.0   U     100    0        0 enx00e04cf0a10c

ip 网络配置工具

https://wangchujiang.com/linux-command/c/ip.html

用来显示或操纵Linux主机的路由、网络设备、策略路由和隧道

支持信息的object

OBJECT := { link | address | addrlabel | route | rule | neigh | ntable | tunnel | tuntap | maddress | mroute | mrule | monitor | xfrm | netns | l2tp | macsec | tcp_metrics | token }

nc 全称netcat

用于设置路由器。它能通过 TCP 和 UDP 在网络中读写数据。通过与其他工具结合和重定向,你可以在脚本中以多种方式使用它。

https://wangchujiang.com/linux-command/c/nc.html

查看端口实例

nc -vz xiaoyuapi.zcoming.com 443 -w2
Connection to xiaoyuapi.zcoming.com 443 port [tcp/https] succeeded!

故障排除

ping 测试主机之间网络的连通性

https://wangchujiang.com/linux-command/c/ping.html

执行ping指令会使用ICMP传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常

traceroute 显示数据包到主机间的路径

https://wangchujiang.com/linux-command/c/traceroute.html

用于追踪数据包在网络上的传输时的全部路径,它默认发送的数据包大小是40字节。

通过traceroute我们可以知道信息从你的计算机到互联网另一端的主机是走的什么路径。当然每次数据包由某一同样的出发点(source)到达某一同样的目的地(destination)走的路径可能会不一样,但基本上来说大部分时候所走的路由是相同的。

traceroute通过发送小的数据包到目的设备直到其返回,来测量其需要多长时间。一条路径上的每个设备traceroute要测3次。输出结果中包括每次测试的时间(ms)和设备的名称(如有的话)及其ip地址。

traceroute www.58.com
traceroute to www.58.com (211.151.111.30), 30 hops max, 40 byte packets
 1  unknown (192.168.2.1)  3.453 ms  3.801 ms  3.937 ms
 2  221.6.45.33 (221.6.45.33)  7.768 ms  7.816 ms  7.840 ms
 3  221.6.0.233 (221.6.0.233)  13.784 ms  13.827 ms 221.6.9.81 (221.6.9.81)  9.758 ms
 4  221.6.2.169 (221.6.2.169)  11.777 ms 122.96.66.13 (122.96.66.13)  34.952 ms 221.6.2.53 (221.6.2.53)  41.372 ms
 5  219.158.96.149 (219.158.96.149)  39.167 ms  39.210 ms  39.238 ms
 6  123.126.0.194 (123.126.0.194)  37.270 ms 123.126.0.66 (123.126.0.66)  37.163 ms  37.441 ms
 7  124.65.57.26 (124.65.57.26)  42.787 ms  42.799 ms  42.809 ms
 8  61.148.146.210 (61.148.146.210)  30.176 ms 61.148.154.98 (61.148.154.98)  32.613 ms  32.675 ms
 9  202.106.42.102 (202.106.42.102)  44.563 ms  44.600 ms  44.627 ms
10  210.77.139.150 (210.77.139.150)  53.302 ms  53.233 ms  53.032 ms
11  211.151.104.6 (211.151.104.6)  39.585 ms  39.502 ms  39.598 ms
12  211.151.111.30 (211.151.111.30)  35.161 ms  35.938 ms  36.005 ms

记录按序列号从1开始,每个纪录就是一跳 ,每跳表示一个网关,我们看到每行有三个时间,单位是ms,其实就是-q的默认参数。探测数据包向每个网关发送三个数据包后,网关响应后返回的时间;如果用traceroute -q 4 www.58.com,表示向每个网关发送4个数据包。

有时我们traceroute一台主机时,会看到有一些行是以星号表示的。出现这样的情况,可能是防火墙封掉了ICMP的返回信息,所以我们得不到什么相关的数据包返回数据。

有时我们在某一网关处延时比较长,有可能是某台网关比较阻塞,也可能是物理设备本身的原因。当然如果某台DNS出现问题时,不能解析主机名、域名时,也会 有延时长的现象;您可以加-n参数来避免DNS解析,以IP格式输出数据。

如果在局域网中的不同网段之间,我们可以通过traceroute 来排查问题所在,是主机的问题还是网关的问题。如果我们通过远程来访问某台服务器遇到问题时,我们用到traceroute 追踪数据包所经过的网关,提交IDC服务商,也有助于解决问题;但目前看来在国内解决这样的问题是比较困难的,就是我们发现问题所在,IDC服务商也不可能帮助我们解决。

mtr My traceroute

https://www.cnblogs.com/alexyuyu/articles/2811346.html

Linux 网络诊断工具MTR(My traceroute) 结合了ping跟tracert的一个工具

首先看最后一行,目标服务器有没有丢包,有的话再往前分析。

测试指定端口实例

sudo mtr -P 443 -i 0.5 -rwc 50 xiaoyuapi.zcoming.com

nslookup

是常用域名查询工具,就是查DNS信息用的命令。

https://wangchujiang.com/linux-command/c/nslookup.html

非交互模式直接加域名

nslookup xiaoyuapi.zcoming.com
Server:		127.0.0.53
Address:	127.0.0.53#53

Non-authoritative answer:
Name:	xiaoyuapi.zcoming.com
Address: 47.92.203.163

telnet 登录远程主机和管理(测试ip端口是否连通)

https://wangchujiang.com/linux-command/c/telnet.html

telnet因为采用明文传送报文,安全性不好,很多Linux服务器都不开放telnet服务,而改用更安全的ssh方式了。但仍然有很多别的系统可能采用了telnet方式来提供远程登录,因此弄清楚telnet客户端的使用方式仍是很有必要的

检测端口,如下表示端口正常

telnet xiaoyuapi.zcoming.com 443
Trying 47.92.203.163...
Connected to xiaoyuapi.zcoming.com.
Escape character is '^]'.

tcpdump

https://wangchujiang.com/linux-command/c/tcpdump.html

 是一款抓包,嗅探器工具,它可以打印所有经过网络接口的数据包的头信息,也可以使用-w选项将数据包保存到文件中,方便以后分析

netstat 查看Linux中网络系统状态信息

https://wangchujiang.com/linux-command/c/netstat.html

可以查看网络协议tcp/udp ip地址监听状态 程序进程id等

netstat -ltnp //查看监听中的进程

ss socket统计信息

https://wangchujiang.com/linux-command/c/ss.html

比 netstat 好用的socket统计信息,iproute2 包附带的另一个工具,允许你查询 socket 的有关统计信息

命令安装

有些发行版可能没有这些命令需要自己安装

net-tools包含以下常用的命令:apt-get install net-tools

  • ifconfig:显示和配置网络接口的信息。
  • netstat:显示网络连接、路由表和网络统计信息。
  • arp:显示和操作ARP缓存。
  • route:显示和操作IP路由表。
  • hostname:显示或设置系统的主机名。

iproute2包含以下常用的命令: apt-get install iproute2

  • ip:用于配置和管理网络接口、路由表、策略路由、隧道、桥接等各种网络参数。
  • ss:显示套接字统计信息,包括网络连接、监听端口、进程关联的套接字等。
  • bridge:用于配置和管理网络桥接。
  • tc:配置和管理流量控制策略。
  • ipset:管理iptables使用的IP集合。
  • nstat:显示网络统计信息。
  • rtacct:显示路由表统计信息。

traceroute命令需要 apt-get install traceroute

网络监控工具

nethogs https://wangchujiang.com/linux-command/c/nethogs.html

//安装
sudo apt install nethogs
sudo yum install nethogs

//使用
sudo nethogs -d 1
sudo nethogs eth0 -d 1
GUI

网络问题排查思路

1.查看服务器服务是否正常,你的网络能否正常访问

当用户反馈APP或网站无法访问或卡顿的时候,首先用你的网络测试一下能否正常访问,

然后排查服务器是否有问题CPU,内存,网络,指标是否正常,进程数量,TCP连接数量 平均负载

各种日志,负载均衡日志,nginx日志,程序日志

2.客户端排查,收集详细的用户反馈

先让用户访问一些常用的网站和app,比如百度等,排除是否是用户的网络问题

收集问题用户数据,手机型号, 操作系统,网络类型(3G,4G,wifi) 运营商(移动,联通,电信),点击哪个模块哪个功能出现的网络故障提示,什么时间点,最好能录屏或者截个图。

或者在程序中增加功能,网络不好的时候弹窗提示用户反馈信息,用户点击反馈按钮收集客户端的ip操作系统型号,上报给服务器并记录下来,通过ip可以获取网络运营商的信息。

3.通过第三方测试平台,模拟用户的网络环境

站长之家测速工具

云服务提供商也用类似站点监控的工具 通过以上工具测试否为运营商问题

4.APP(移动测试)远程真机调试

参考

手机远程真机调试平台汇总
有哪些好用的App云测试平台

目前对移动App的测试主要指的是下面几部分:

兼容性测试——App对不同手机、操作系统版本的兼容性测试,包括安装、启动、卸载等。
功能测试——遍历应用的每一个角落,查看应用的功能、逻辑是否正常,完整。
性能测试——应用的性能怎样,如启动时间、反应时间、CPU占用率,内存占用率等。
稳定性测试——在一定时间内对App进行持续地测试,测试App运行的稳定性。
网络场景测试——测试不同网络环境中App的运行状况,如2G&3G,弱网络等。

Testin:Testin云测|全球领先的App及手游测试平台

腾讯优测:优测网-让测试更简单

贯众云测试:贯众云测试

百度MTC:百度移动云测试中心

阿里MQC:MQC 阿里移动质量中心

参考


ORM扣库存导致的BUG总结

前一段时间,以前一个项目的积分兑换功能出现了库存超扣现象,排查问题时,一看更新时没有加锁,于是以为是锁的问题,加锁之后没有仔细推敲就汇报bug修复完成.

//初始代码
DB::transaction(function () use ($request, $user_model) {
 $goods_model = Goods::find($request->id);
 if ($goods_model->stock <= 0 || $goods_model->stock < $request->number) {
          throw new \Exception('库存不足');
         }
  //扣库存
 $goods_model->stock-=$request->number;
 $goods_model->save();
 ...
});

//第一次修复BUG,增加悲观锁 for update
DB::transaction(function () use ($request, $user_model) {
 $goods_model = Goods::lockForUpdate()->find($request->id);
 if ($goods_model->stock <= 0 || $goods_model->stock < $request->number) {
          throw new \Exception('库存不足');
         }
  //扣库存
 $goods_model->stock-=$request->number;
 $goods_model->save();
 ...
});

昨天接的报告,库存超扣BUG又出现了,立即查看Binlog 日志排查sql执行记录

看日志,同一时刻,有两个线程(386609669,370064926)更新库存,线程386609669 先将库存更新为2 ,线程370064926随后又将库存更新为9 ,并且响应时间760ms ,猜想是数据库响应问题,正考虑其它解决方案,队列或限流等.

看日志nginx,发现在抢购兑换时间内只有几十的并发,看了一下RDS的资源统计使用率不高,感觉悲观锁可以应付的来,不是性能问题.

继续看binlog 日志,发现库存被 线程370064926更新为9之后,又被其它线程386850322更新为3,继续往后看发现同一时间内有好多线程在同时更新库存而且更新库存数量不相同.

百思不得其解,网上Google Mysql高并发扣库存方案,其中有一只乐观锁方案,提到在事物中更新库存时,要比对程序中的库存数量和mysql中的库存数量,突然间恍然大悟,仔细一看代码.果然,犯了一个相当愚蠢的错误.

DB::transaction(function () use ($request, $user_model) {
 $goods_model = Goods::lockForUpdate()->find($request->id);
 if ($goods_model->stock <= 0 || $goods_model->stock < $request->number) {
          throw new \Exception('库存不足');
         }
  //扣库存
 $goods_model->stock-=$request->number; 标注1
 $goods_model->save();
 
 ...
});

问题分析

标注1

更新使用的是程序模型中的库存数量 – 兑换数量

假设stock为 9 number 为 2
上述写法生成的sql 为 update goods set stock = 7 where id = ?
在高并发场景下这么写显然是非常错误的. 假设商品的初始库存为10,ABC三个用户同一时刻兑换商品,ABC读取到程序模型中的库存数量 $goods_model->stock 都是10 ,由于用了悲观锁for update(这不是重点,不用悲观锁也会出现这种情况) 更新语句变为阻塞执行,用户A将库存改为9,用户BC同样将库存改为9,最后库存剩余还是9,这显然与预期结果不服,正确的剩余库存应该为7

扣库存应该采用 Update goods set stock = stock -1 where id = ? 这种语句,更新时读取的是当前记录的stock的值.最后修正结果代码如下:

//最后修正
DB::transaction(function () use ($request, $user_model) {
 $goods_model = Goods::lockForUpdate()->find($request->id);
 if ($goods_model->stock <= 0 || $goods_model->stock < $request->number) {
          throw new \Exception('库存不足');
         }
  //扣库存 正确的操作 或者使用原生update
  $goods_model->decrement('stock');
 ...
});

decrement 方法生成的语句为update goods set stock = stock -1 where id = ?

总结

  • 外包项目写多了忽略了程序在高并发场景下可能发生的情况.高并发场景要考虑进程线程之间数据一致性和以及程序和数据库(包含nosql) 的数据一致性问题
  • 遇到问题要反复仔细验证,不要轻易下结论

疑问

使用错误的的写法用JMeter模拟并发兑换 20threads/1s 200threads/1s 都没有出现库存超扣现象,由于生产环境不允许压测,所以在本机测试 .猜测可能本机的配置高于服务器,处理能力高于服务器所有没有复现,以后验证成功回头再补充

本机(8核16G) 服务器(4核8G)而且跑了很多项目

解决方案引申

乐观锁

//乐观锁
    public function caseOne(Request $request)
    {
        $this->validate($request, [
            'number' => 'bail|required|integer|min:1',
            'goods_id' => [
                'bail', 'required', 'integer',
                Rule::exists('goods', 'id')->where(function ($query) use ($request) {
                    $query->where('stock', '>=', $request->number);
                }),
            ]
        ]);
        DB::transaction(function () use ($request) {
            $goods = Goods::find($request->goods_id);
            $result = DB::update('update goods set stock = stock - ? where id = ? and stock >= ?', [$request->number, $request->goods_id, $request->number]);
            if (!$result) {
                return response()->json(['message' => '失败']);
            }
            Orders::create([
                'goods_id' => $goods->id,
                'goods_name' => $goods->name,
                'number' => $request->number,
            ]);
        });

        return response()->json();
    }

悲观锁

//悲观锁
    public function caseTwo(Request $request)
    {
        $this->validate($request, [
            'number' => 'bail|required|integer|min:1',
            'goods_id' => [
                'bail', 'required', 'integer',
                Rule::exists('goods', 'id')->where(function ($query) use ($request) {
                    $query->where('stock', '>=', $request->number);
                }),
            ]
        ]);
        DB::transaction(function () use ($request) {
            $goods = Goods::lockForUpdate()->find($request->goods_id);
            $result = DB::update('update goods set stock = stock - ? where id = ? and stock >= ?', [$request->number, $request->goods_id, $request->number]);
            if (!$result) {
                return response()->json(['message' => '失败']);
            }
            Orders::create([
                'goods_id' => $goods->id,
                'goods_name' => $goods->name,
                'number' => $request->number,
            ]);
        });

        return response()->json();
    }

不用事务

//不用事务
    public function caseThree(Request $request)
    {
        $this->validate($request, [
            'number' => 'bail|required|integer|min:1',
            'goods_id' => [
                'bail', 'required', 'integer',
                Rule::exists('goods', 'id')->where(function ($query) use ($request) {
                    $query->where('stock', '>=', $request->number);
                }),
            ]
        ]);
        $goods = Goods::find($request->goods_id);
        $result = DB::update('update goods set stock = stock - ? where id = ? and stock >= ?', [$request->number, $request->goods_id, $request->number]);
        if (!$result) {
            return response()->json(['message' => '失败']);
        }
        Orders::create([
            'goods_id' => $goods->id,
            'goods_name' => $goods->name,
            'number' => $request->number,
        ]);
    }

redis库存

//redis库存
    public function caseFour(Request $request)
    {
        $this->validate($request, [
            'number' => 'bail|required|integer|min:1',
            'goods_id' => [
                'bail', 'required', 'integer',
                function ($attributes, $value, $fail) use ($request) {
                    $stock = Redis::get('goods_id_' . $value);
                    if (is_null($stock)) {
                        return $fail('商品不存在');
                    }
                    if ($stock < $request->number) {
                        return $fail('库存不足');
                    }
                }
            ]
        ]);
        $goods = Goods::find($request->goods_id);
        $result = DB::update('update goods set stock = stock - ? where id = ? and stock >= ?', [$request->number, $request->goods_id, $request->number]);
        if (!$result) {
            return response()->json(['message' => '失败']);
        }
        Orders::create([
            'goods_id' => $goods->id,
            'goods_name' => $goods->name,
            'number' => $request->number,
        ]);
        Redis::decr('goods_id_' . $request->goods_id);

        return response()->json();
    }

队列

public function caseFive(Request $request)
    {
        $this->validate($request, [
            'number' => 'bail|required|integer|min:1',
            'goods_id' => [
                'bail', 'required', 'integer',
                function ($attributes, $value, $fail) use ($request) {
                    $stock = Redis::get('goods_id_' . $value);
                    if (is_null($stock)) {
                        return $fail('商品不存在');
                    }
                    if ($stock < $request->number) {
                        return $fail('库存不足');
                    }
                }
            ]
        ]);
        $goods = Goods::find($request->goods_id);
        SecKill::dispatch($goods, $request->all());

        return response()->json();
    }

https://github.com/yangliuan/shop-demo 有待完善

微博删除脚本

// ==UserScript==

// @name Weibored.js

// @namespace http://vito.sdf.org

// @version 0.2.0

// @description 删除所有微博

// @author Vito Van

// @match http://weibo.com/p/*

// @grant none

// ==/UserScript==

'use strict';

var s = document.createElement('script');

s.setAttribute(

'src',

'https://lib.sinaapp.com/js/jquery/2.0.3/jquery-2.0.3.min.js'

);

s.onload = function() {

setInterval(function() {

if (!$('a[action-type="feed_list_delete"]')) {

$('a.next').click();

} else {

$('a[action-type="feed_list_delete"]')[0].click();

$('a[action-type="ok"]')[0].click();

}

// scroll bottom let auto load

$('html, body').animate({ scrollTop: $(document).height() }, 'slow');

}, 800);

};

document.head.appendChild(s);

打开微博个人首页.F12打开控制台,输入代码回车

Laravel Config 缓存原理

参考

加载过程

  • laravel的所有配置文件都在config目录下
  • 启动时加载所有config目录下的所有配置文件
  • 通过Dotenv 类库加载.env文件中的配置项到预定义全局变量$_ENV中
  • env函数中使用getenv()来获取环境变量$_ENV中的值
  • config文件中使用env()加载配置值
  • config()函数调用config文件中的配置项

使用配置文件时要注意严格遵守约定,在config文件中调用env()函数

在路由,控制器和模型以及自定义类文件中必须使用config()函数获取配置项的值

Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
 /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        //此处判断是否开启缓存配置,开启缓存配置直接返回缓存中的配置
        //因此开启缓存配置后,env()函数会失效
        if ($app->configurationIsCached()) {
            return;
        }

        $this->checkForSpecificEnvironmentFile($app);
        //此处加载.env中的文件
        try {
            (new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
        } catch (InvalidPathException $e) {
            //
        }
    }
/**
     * Gets the value of an environment variable.
     *
     * @param  string  $key
     * @param  mixed   $default
     * @return mixed
     */
    function env($key, $default = null)
    {
        $value = getenv($key);

        if ($value === false) {
            return value($default);
        }

        switch (strtolower($value)) {
            case 'true':
            case '(true)':
                return true;
            case 'false':
            case '(false)':
                return false;
            case 'empty':
            case '(empty)':
                return '';
            case 'null':
            case '(null)':
                return;
        }

        if (strlen($value) > 1 && Str::startsWith($value, '"') && Str::endsWith($value, '"')) {
            return substr($value, 1, -1);
        }

        return $value;
    }
/**
     * Get / set the specified configuration value.
     *
     * If an array is passed as the key, we will assume you want to set an array of values.
     *
     * @param  array|string  $key
     * @param  mixed  $default
     * @return mixed|\Illuminate\Config\Repository
     */
    function config($key = null, $default = null)
    {
        if (is_null($key)) {
            return app('config');
        }

        if (is_array($key)) {
            return app('config')->set($key);
        }

        return app('config')->get($key, $default);
    }
//app('config) 生成的是 Illuminate\Config\Repository 实例

开启缓存配置

php artisan config:cache

/**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {  
        //调用清楚缓存命令
        $this->call('config:clear');
       //加载所有配置项
        $config = $this->getFreshConfiguration();
       //使用var_export导出php可以执行的数组
       //文件系统使用file_put_contents 生成 配文件 config.php
       //配置文件缓存目录 app/bootstrap/cache/config.php
        $this->files->put(
            $this->laravel->getCachedConfigPath(),
            '<?php return ' . var_export($config, true) . ';' . PHP_EOL
        );

        $this->info('Configuration cached successfully!');
    }

php artisan config:clear

/**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {   
        //laravel文件系统使用 unlink 删除 缓存的config.php文件
        $this->files->delete($this->laravel->getCachedConfigPath());

        $this->info('Configuration cache cleared!');
    }

总结

处理思想就是把多个配置文件合并成一个配置文件,减少I/0操作提升性能.

UbuntuServer 18.04以上安装教程

1.请选择首选语言

2.请在下面选择您的键盘布局,或选择“识别键盘”以自动检测您的布局

3.欢迎使用Ubuntu ,世界上最受欢迎的 云,集群,和令人惊叹的物联网平台.这是ubuntu在服务器和网络设备上的安装

  • 1.安装ubuntu
  • 2.3 云平台安装 MAAS https://maas.io

4.配置网卡

5.配置代理

不需要代理直接 选择Done

选择镜像地址

推荐使用国内镜像,国外镜像由于墙的问题可能安装失败

https://mirrors.aliyun.com/ubuntu/

文件系统安装
  • 使用整个磁盘
  • 使用真个磁盘并且换装LVM(推荐)
  • 手动安装
选择磁盘安装
磁盘选项

采用默认配置

资料设置
选择安装openssh server

选择软件跳过,进入安装

重启进入登录界面,输入用户名密码

安装增强功能配置并共享文件夹

1.更新系统软件

sudo apt update && sudo apt upgrade

2.安装增强功能

设备>>安装增强功能

点击右下角光驱选择 /usr/share/virtualbox/VBoxGuestAdditions.iso

3.安装工具 dkms build-essential

sudo apt-get install dkms 
sudo apt-get install build-essential

4.挂在光驱

 sudo mount /dev/cdrom /mnt/
挂在成功

5.安装增加功能包

sudo /mnt/VBoxLinuxAdditions.run

6.配置共享文件夹(点击虚拟机设置或右下角文件夹图标)

挂载点名称

7.挂载磁盘 sudo mount -t vboxsf [挂在点名称] [挂载后的本地路径]

sudo mount -t vboxsf downloads /home/www/share

开启root登录

#设置root密码
sudo passwd root

vim /etc/ssh/sshd_config
添加#注释掉PermitRootLogin Prohibit-password
添加:PermitRootLogin yes

#重启sshd
serivce sshd restart

Laravel框架读写分离测试

#配置修改
 'mysql' => [
            'read' => [
                'host' => '192.168.31.194',
            ],
            'write' => [
                'host' => '127.0.0.1',
            ],
            #同一请求周期内使用相同链接获取数据
            'sticky'    => true,
            'driver'    => 'mysql',
            'database'  => 'lara',
            'username'  => 'root',
            'password'  => '123456',
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix'    => '',
  ],

测试代码

public function create()
    {
        dump(Order::count());
        $res = Order::create(['sn' => date('YmdHis') . mt_rand(0000, 9999)]);
        dump(Order::find($res->id), Order::count());
    }

stop slave; 关闭从库同步

sticky 为 false时

sticky 为 true时

写入之后的读取的count数为47 插入的id模型也打印成功,说明laravel在写入操作之后使用的是写连接.

源码分析

Illuminate\Database\Connection.php

/**
     * Get the current PDO connection used for reading.
     *
     * @return \PDO
     */
    public function getReadPdo()
    {
        if ($this->transactions > 0) {
            return $this->getPdo();
        }

        if ($this->getConfig('sticky') && $this->recordsModified) {
            return $this->getPdo();
        }

        if ($this->readPdo instanceof Closure) {
            return $this->readPdo = call_user_func($this->readPdo);
        }

        return $this->readPdo ?: $this->getPdo();
    }

代码判断配置sticky 为true 并且数据修改成功返回当当前链接