SegmentFault suntao10最新的文章
2021-01-25T16:14:03+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
朝花夕拾 - 算法
https://segmentfault.com/a/1190000039084322
2021-01-25T16:14:03+08:00
2021-01-25T16:14:03+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>关键词:滑动窗口、广度优先、深度优先、二叉树、动态规划</p>
朝花夕拾 - 面向对象设计模式
https://segmentfault.com/a/1190000039084312
2021-01-25T16:12:38+08:00
2021-01-25T16:12:38+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>关键词:创建型模式、结构型模式、行为模式、SOLID原则</p>
朝花夕拾 - 分布式架构
https://segmentfault.com/a/1190000039084262
2021-01-25T16:11:04+08:00
2021-01-25T16:11:04+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>关键词:DTP(2PC|3PC)、拆分微服务原则、一般性架构设计、Promethus监控、ZIPKIN分布式日志、ELK日志收集、sentry日志预警、haproxy/nginx反向代理、服务注册与发现(consul|zookeeper|euraka)、服务治理、kong网关、配置中心(zookeeper|euraka)、ES搜索引擎、MDC设计</p>
朝花夕拾 - 网络协议
https://segmentfault.com/a/1190000039084196
2021-01-25T16:07:02+08:00
2021-01-25T16:07:02+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>关键词:OSI、IP报文、TCP报文、UDP报文、MSS与窗口、拥塞控制、ARP与RARP、NAT与DHCP、三次握手四次挥手、ICMP、https、http2</p>
朝花夕拾 - PHP二三事
https://segmentfault.com/a/1190000039083917
2021-01-25T15:52:08+08:00
2021-01-25T15:52:08+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>关键词:PHP架构、垃圾回收、数组底层实现、变量结构zval、运行过程、fpm优化、PSR1-2的内容、PHP多进程通信、laravel原理篇(容器、路由、队列、服务提供者)、swoole多协程、SPL等</p><p>1.PHP的基础架构<br>PHP由底层到PHP应用依次是:zend_engine->zend(api/extension)->php(api/extension)->SAPI->application<br>php的启动过程:<br>模块初始化->请求初始化->词法分析->语法分析->中间码生成->zend引擎分析中间码,生成机器码->CPU执行->请求shutdown->模块shutdown</p><p>2.垃圾回收<br>PHP的垃圾回收是分成4步,每一步会被标记为不同的颜色:<br>1.将缓存区中的所有可能跟标记为紫色<br>2.执行模拟删除操作,这一步会将ref_count中大于0的部分-1,会得到两种结果,一种是>0一种是=0<br>3.执行模拟恢复造作,这一步会将上一步中的ref_count>0的部分恢复掉<br>4.执行删除</p><p>3.数组的底层实现<br>数组的底层是哈希表,哈希表的内容存放在arData中,arData是一个数组,当需要存入新的键值对时,需要对key哈希,得出一个中间映射表的nIndex并存入arData数组下标,依靠中间映射表完成哈希查找,依靠arData完成顺序遍历。<br>哈希冲突:arData中主要是bucket存放key-value,这是一个链表结构,当有冲突时,会直接将新key-value加到头部,连上旧的链表,查找时需要遍历链表,如果没有冲突,当新查找时,返回的就是链表头部。<br>写入的过程:写入时会直接插入arData数组中,数组是一块连续的内存空间,直接追加,速度很快,之后会带上bucket的下标,根据n|TableMask得到一个nIndex,这个就是中间映射表的位置,在该位置上存放bucket下标,就完成了nIndex和bucket的映射。<br>扩容:当nNumUsed(nElements是有效元素)不够时,会扩容:<br>新申请的空间会是旧nNumUsed的2倍,然后从旧空间中迁移数据到新空间中,当迁移完毕后,旧空间会被清空。<br>删除:bucket中的元素会被标记为IS_UNDEF,这个是删除标记,等待扩容时会自动删除。</p><p>4.变量结构ZVal<br>zVal包括type字段,PHP中的类型转换就是通过这个字段来完成的,type字段包括IS_BOOL IS_DOUBLE IS_INT IS_NULL等来标记,在PHP7之前,zVal中的数据有可能会是一个指针,这个指针所指向的变量可能还会是一个指针,所以在cpu计算的时候,会因为缓存区上没有该变量实际的值而去从内存中拿,这样内存和CPU来回浪费了很多时间,而在PHP7中修改了zVal的数据结构,将val和zval放在一起,在CPU执行时就可以在缓存中读取到,这也是Php7比5.6快的原因之一。</p>
朝花夕拾 - rabbitmq消息队列
https://segmentfault.com/a/1190000039081661
2021-01-25T12:44:56+08:00
2021-01-25T12:44:56+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>关键词:事务、确认、信道、exchange类型、普通集群、镜像集群</p><p>1.事务<br>rabbitmq支持事务,事务的语句为tx.Select() tx.Commit() tx.Rollback(),事务对吞吐量影响会比较大,如果为了数据安全考虑,可以开启事务,防止消息丢失。</p><p>2.确认<br>有单条消息确认、多条消息确认和异步确认机制。确认也是为了避免数据丢失的一种办法,但是相比于事务,确认机制对性能影响较小</p><p>3.信道<br>信道是基于TCP的一种特殊的链接,每一个producer都有一个惟一的ChannelId,不同的Producer可以复用同一个TCP channel,为了提高消息吞吐量,避免网络连接的创建和销毁带来的额外负载。</p><p>4.exchange类型<br>exchange主要分为三种类型:<br>direct:就是routingkey和队列一一对应,对应的消费者只能消费这一个生产者生产的消息。</p><p>topic:通过通配符匹配消费的队列,例如:生产者生产了一条hello.world的消息,那么消费者通过*.world或者#可以匹配到这条消息,并进行消费。</p><p>fanout:广播消息,此时的routingkey并没有什么用了,只要生产者生产消息到这个交换机上,连入这个交换机的所有消费者都可以消费到这条消息。</p><p>5.普通集群<br>rabbitmq为了扩展负载能力和处理速度,可以分多台不同的rabbitmq-broker部署在不同的节点上,分别负责不同的业务。集群中的各台机器都需要与其他机器同步集群元数据,常见的集群元数据包括:<br>队列元数据<br>绑定元数据<br>vhost元数据<br>交换机元数据<br>在普通集群中, 必须要至少保证有一个硬盘节点,因为硬盘节点负责持久化元数据,当集群中有一台机器宕机后重启,如果是内存节点是不可以恢复原来的数据的,所以需要有一台硬盘节点,存储所有集群的元数据,而在集群中如果硬盘节点挂机了,那么集群中是不会接受对元数据的修改请求的,但是生产消费已经有元数据的消息是可以的。</p><p>6.镜像集群<br>普通集群不能保证节点宕机后的高可用,所以为了让集群实现高可用,还需要对节点进行副本部署,与主节点进行数据同步,以便在master节点宕机后由从节点替代master接入新的请求。<br>镜像集群的结构是一个环状列表,当请求打入master后,master会将该消息通过GM组播通讯协议传递给集群中的每一个节点,这些节点像一个环状链表一样串联起来,由相邻的节点同步传递消息,当master收到自己传递的消息之后,就说明镜像集群的所有节点已经同步完成,此时就可以返回ack给客户端了。<br>当有新节点加入时,需要根据配置ha.manual或者自动同步新节点,如果是手动的,那么在新节点加入之后需要手动操作才能完成数据同步,否则需要等待。而自动则会随着节点的加入而自动同步数据。<br>新节点同步数据时,因为以前没有消息,所以可能会需要写入大量数据才能完成同步,所以究竟选择手动还是自动,需要根据生产的部署而自动斟酌。</p>
朝花夕拾 - kafka消息队列
https://segmentfault.com/a/1190000039081637
2021-01-25T12:39:38+08:00
2021-01-25T12:39:38+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>关键词:批处理、pageCache、多partition_log、顺序读写、零拷贝、ISR、LEO和HW、consumer和partition、zookeeper的作用、实现顺序生产和消费、exactly once语义、request.required.acks、避免消息丢失的方案</p><ol><li>批处理</li></ol><p>producer端发送消息的步骤如下:</p><ul><li>封装消息体producerRecord</li><li>序列化这个对象avro</li><li>确认分区(指定partition、keyhash、roundrobin、随机之类的)</li><li>放入到批次中,一个batch缓冲区的大小是32M,一个batch为16K,batch的数据结构为topic=>消息队列</li><li>sender线程从缓冲区拿到可发送的batch发送给broker</li></ul><p>2.broker接收消息<br>broker接收消息会先放入到pagecache中,随后有刷盘策略进行刷盘,通过producer.type控制是同步还是异步,还可以通过flush.ms、flush.messages来确定刷盘的时机。在brokerlist中有ISR集群,leader写入之后需要同步日志给其他的ISR副本,直到全部写入完成之后,leader才会返回ack给producer。<br>顺序读写磁盘:单个partitionlog是顺序写入磁盘的,顺序写入磁盘可以最大化发挥磁盘性能,速度比内存随机读要快。<br>零拷贝写磁盘:mmap实现,是指用户空间在内核空间操作数据,刷磁盘等。<br>零拷贝读磁盘:sendfile实现,是指DMA技术使网卡直接在内核中读取buffer并传入网络。</p><p>3.多partitionlog并行写入<br>一个topc下可以由多个不同的partiion分布在不同的broker上,这样当有多个producer时,也可以并行得写入消息日志。</p><p>4.ISR集合<br>每一个partiion都有一个leader和多个replica副本,其中规定一部分副本必须与leader保持日志的同步,处于同步状态下的replica就是ISR集合,同步状态集。</p><p>5.HW和LEO<br>HW是leader上标记的ISR集合中最小LEO的位置,这个位置也是consumer可见的最大offset所在,当新消息写入leader时,这个HW并不会立即偏移到LEO所在位置,而是需要等待ISR集合中的所有副本全部写完之后,才会将HW偏移到LEO的位置。</p><p>6.consumer和partition<br>consumer需要在一个consumer-group中对partition_log进行消费,当新consumergroup建立时,broker会有一个机器作为协调器从consumer-group中选出一个消费者成为主消费者,这个主消费者往往是第一次接入的那台机器,这个机器会负责从协调器中拿到所有consumer和partition的信息,然后通过roundrobin和range方式对consumer确认partition的消费关系。各个consumer从协调器中获取自己与partition的关系,然后进行消息的消费。</p><p>7.zookeeper的作用<br>zookeeper主要用于存储一些基础的元信息,如brokerlist、producer、consumer、consumer-partition、ISR、offset</p><p>8.如何实现顺序消费<br>对于同一个partitionlog而言,消息自然是有序的,那么在生产消息和消费消息的时候,可以将消息写入这个partitionlog中,然后实现一个顺序生产和消费的策略。</p><p>9.request.required.ack<br>-1:表示所有ISR集合全部写入日志成功<br>0:写入leader与否不管,直接返回<br>1:写入leader即可,不管是否ISR已经全部写入<br>-1是保证数据安全不丢失的最好选项,但是会影响吞吐量,在此问题之上,可以考虑缩小ISR的数量,但是不能没有ISR,此外,网络和硬件也可以提高吞吐量</p><p>10.避免消息丢失</p><ul><li>producer一侧:发送确认机制,如果发现因为网络原因,或broker宕机导致发送消息失败,那么会再次重试发送,当然,这里也应该设置producer.type为同步,因为异步并不能很好解决这个问题。</li><li>broker:如果有消息未写入ISR集合中的所有成员,那么也不会发送ack给producer,如果leader宕机了,那么ISR中的其他成员也会接替leader,拥有leader的所有消息,所以也不会丢失消息,如果leader未来得及同步消息给ISR集合中的replica的话,那么这个消息将继续等待同步,否则不会ack给producer</li><li><p>consumer:如果消息提交offset是自动的,那么确实很容易就出现消息丢失,如果手动提交,又有三种语义状况:</p><ul><li>最少一次</li><li>最多一次</li><li>严格一次</li></ul></li></ul><p>严格一次的保证,是靠将offset提交动作与事务提交动作绑定再一起,实现思路就是让offset保存在数据库中,依靠事务一起提交或者一起回滚,当然这样的话会对吞吐量造成很大影响,所以可以使用缓存,事务提交之后,将offset记录到缓存中,当再次消费该消息的时候,检查offset是否在缓存中,如果offset已经在缓存中了,就表明不需要重复消费了。</p><p>11.partitionlog文件存储格式<br>partitionlog是顺序存储的,文件存储是segment文件,里面有index和data量个文件,index是索引文件,data是消息记录的文件,文件名是20位的数字,这个是上一个segmentoffset的最大值,里面记录的是相对于自己的offset和对应的消息物理地址偏移量,所以在查找的时候,是靠二分查找到文件,再二分查找到offset,得到物理偏移量,读取相应文件。</p>
朝花夕拾 - Mysql那些事
https://segmentfault.com/a/1190000039081587
2021-01-25T12:30:47+08:00
2021-01-25T12:30:47+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>Mysql那些事<br>关键词:事务与日志、隔离级别、锁(死锁)、索引、InnoDB_buffer_pool、双写机制、主从同步、集群方案、分库分表、explain分析</p><ol><li><p>MySQL事务:<br>mysql事务只能存在与Innodb引擎之下,InnoDB引擎为了保证事务的ACID特性,专门有redo_log和undo_log来保证持久性和原子性,隔离性是靠事务隔离级别和锁机制来保证的,而持久性、原子性、隔离性都是为了保证一致性。<br>什么是一致性?是指数据从一个确定的状态转移到另外一个确定的状态。这样太抽象了,可以直接举例:A给B转了100块,A账户要少100块,B账户上要多100块,并且还能表示出这100块是从A转给B的,这样就是一个确定的状态。<br>redo_log:重做日志,保证持久性而存在的产物,有固定的文件大小和数量,轮询写入,一般是2个文件,大小为128M,记录的是关于页的物理逻辑日志,所以恢复起来会非常快,里面的轮询写入是指,当一个文件写完了,就直接跳到另一个文件的头部重新覆盖,这样做得好处就是节省磁盘空间,因为对redo来说,数据只要刷盘成功,redolog的历史记录就失去了他的意义,所以只记录有限大小,如果在高并发数据库中写,可以适当增加文件数量。注意这里的物理逻辑日志,等会说双写机制的时候会提到,redo_log的刷盘策略是根据innodb_flush_log_at_trx_commit=0、1、2来确定的。</p><ul><li>0: 每秒从redo_buffer->os_cache->flush(fsync操作)</li><li>1: 每次事务提交 redo_buffer->os_cache->flush</li><li>2: 每次提交事务 redo_buffer->os_cache 每秒flush</li></ul></li></ol><p>undo_log是保证事务的原子性的,也是随同事务的周期来写入的,在RC隔离级别以上,会有MVCC来避免脏读,RR隔离级别之上,会有快照读和当前读+next-key_lock来避免幻读。<br>MVCC: 多并发版本控制,当我们读取到一行数据时,MVCC会在这行数据多加两个列:当前事务ID和回滚ID。当需要查询数据的时候,会查找事务ID小于当前事务ID和回滚ID大于当前事务ID的数据。当更新时,会将旧行数据复制一行,并将更新后的值更改掉,加上当前事务ID,将旧行的回滚ID写入当前事务Id,当新插入数据时,会直接将当前事务ID加在对应字段上。<br>快照读和当前读:<br>快照读:事务开始查询时,将查询结果缓存在buffer中,后续的读取就从buffer+undolog结合来进行。<br>当前读:事务在执行update、insert、for update、lock in share mode时,会直接操作现有数据,通过next-key lock来避免幻读。</p><p>2.双写:<br>双写机制是为了避免脏页刷到磁盘上因为机器断电而出现部分写失效的问题,所以为了解决这个问题,双写的过程如下:</p><ul><li>将数据脏页写在double_write_buffer中,这个buffer大小一般是2M,</li><li>在共享表空间上分配连续两个区,也是2M</li><li>将double_write_buffer中的数据刷入共享表空间中的两个区上,因为是顺序写,性能很快</li><li>再将double_write_buffer中的数据刷入硬盘中,此时是离散写,性能会很慢。</li></ul><p>通过利用共享表空间的顺序写机制,刷盘动作不容易出现写一半出错的情况,下面说恢复时的过程:<br>redo_log用来记录数据页上的物理日志,所以redolog必须要定位到页,然后校验check_sum,才能进行数据恢复。所以在恢复时,首先会找到破损页,因为check_sum不通过,所以会从共享表空间中最近写入的两个区找到对应页的数据重写入这个页中,然后再校验checksum,判断check_point,开始从事务开始的地方开始重做。</p><p>3.锁<br>锁的分类可以由以下几种:<br>表锁:IS锁,IX锁<br>行锁:Record lock<br>间隙锁:Gap lock<br>临键锁:next-key lock<br>共享锁、排他锁(读锁、写锁)<br>乐观锁、悲观锁(概念)<br>死锁(现象)<br>死锁的几种情况:</p><ul><li>行X锁互相等待,导致死锁</li><li>Gap锁与插入意向锁互相等待,导致死锁</li><li>X锁与S锁互相等待,导致死锁</li></ul><p>如何表面死锁?除了上述说的几种情况之外,要尽量减少锁粒度,给事务加锁的种类,要尽量单一,不要出现一个事务多种锁的情况。</p><p>4.索引<br>B+树索引,哈希索引。<br>B+树索引,叶子节点存放所有数据,子节点存放索引冗余和其他节点的指针,查找时从头部节点开始向下查找,查找的时间复杂度为O(lgN),这个值会直接影响索引检索时的硬盘IO次数,树的深度越深,IO次数越多。在B+树上,数据是从小到大排列的,全局有序,所以可以用来做排序,范围查找。<br>哈希索引:查找时间复杂度为O(1),理想状况下,但是,哈希索引只能是查找单个元素比较快,如果放在范围,条件判断这类查找时,哈希索引就不行了,更别说还要满足排序了。</p><p>5.Innodb_buffer_pool<br>Innodb_buffer_pool的三个比较重要的list: free_list、lru_list、flush_list,free_list就是空间内存块链表,lru_list就是顾名思义,最近访问内存链表,flush_list就是脏页刷新链表,当需要从这个buffer_pool中加载数据的时候,首先会根据spaceId+pageNumber来确定要访问的页,然后从自己的pool中查看是否有该页的数据,(lru_list),如果有该页数据的话,那么查找到之后,直接从中加载对应数据,查找不到的话,需要从硬盘中加载数据,那么加载完数据之后,需要放入到buffer_pool中,buffer_pool中需要找到对应的空闲内存块,于是先从free_list中寻找可用的空闲内存块,如果没有可用的空闲内存块,则从lru_list中的尾部开始向前查找,是否有可以删除掉的空闲内存快,如果有就放入到free_list中,同样,这里查找不到的话, 就从flush_list中查找是否可以刷盘,刷盘后的内存块会放入到free_list中去。</p><blockquote>注意这里的脏页,刷到磁盘上时,其实是先要刷到double_write_buffer中的,然后才开始写入磁盘。下面的,就如上双写内容了。</blockquote><p>6.主从同步<br>从库有一个IO thread会向master发起一个同步请求,master会将本地的binlog发送给这个线程,这个线程会将binlog写入到relay_log中,slave还会启动一个sql thread来专门重放relay_log,这样的话,slave就相当于重做了一遍主库的数据操作。</p><p>7.集群方案<br>一般为M-S读写分离的集群架构,但是单单的M-S是不支持故障转移的,所以需要借助其他的工具,如一些代理组件来接入数据库,路由并完成故障转移。<br>M-S架构中,S同步时可能会对M同步时造成M的硬盘负载比较大,所以需要用其中一个slave机器用作binlog中继,即一个blackhole引擎的节点,这个blackhole引擎的节点负责同master复制binlog,然后,其他的slave从这个blackhole节点中同步binlog,blackhole因为其只同步binlog但不写入表的特性,硬盘IO较小。</p><p>其他的集群方案,有MHA,将各个M-S分成一个独立的group,然后各个group之间会有同步,这样的话,可以提高故障时的高可用,也可以完成读写分离的需要。MHA是日本某公司基于MySQL-cluster做出来的一个集群方案,可用性较好。</p><p>MySQL-Cluter,是MySQL官方出的一个集群方案, 这个集群方案中,需要将引擎换为NDB,这一点对于老的数据节点来说很不友好。</p><p>8.分库分表<br>对于单表而言,分库分表也即将一张大表拆分成小表,有两种办法:水平拆分和垂直拆分。<br>水平拆分:将数据量大的表,拆分成多个业务表,前端可以路由获取对应的表中的数据,路由规则可以是对全局主键hash处理。<br>对于业务来说,应该是不同的业务采用不同的库,避免不同业务压力聚集到单个库中。业务量大的库,可以单独部署集群或拆分成小业务并读写分离。</p><p>9.explain<br>其中几个比较关键的列:<br>select_type type key possible_keys key_len rows<br>select_type查询类型:simple、unioin、subquery之类的类型<br>type systemconstrefeq_refrangeindexall system<br>key 用到的索引<br>possible_keys 可能索引<br>key_len 索引长度<br>rows影响行数</p>
朝花夕拾 - redis知识要点
https://segmentfault.com/a/1190000039081520
2021-01-25T12:21:37+08:00
2021-01-25T12:21:37+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
0
<p>关键词:线程模型与多路复用、底层数据结构、RDB/AOF、redis-cluster、一致性哈希环、哨兵、缓存穿透/击穿/雪崩、读写分离和本地缓存、分布式锁、事务和管道,内存淘汰策略,缓存过期策略,其他数据结构</p><blockquote>本文是无图文章</blockquote><p>导语:Redis由于其丰富的数据结构和基于内存、Reactor单线程模型和统一的IO多路复用模型,以及redis-cluster,使得redis既能作为缓存内存为访问加速,又可以作为Nosql存储较为简单的数据库。</p><p>1.Redis的线程模型与多路复用</p><p>Redis使用单线程Reactor处理请求事件,所产生的的读写连接事件等会交给专门的事件处理器来进行处理,在redis中,这种事件处理器是在同一个线程中运行的。<br>多路复用:redis使用同一的接口封装了系统的selectepollkqueue等不同的多路复用网络IO模型,使得redis在任何系统中都可以有比较高的性能,封装的接口函数包括aeCreatexxx、aePollxxx之类的函数,里面是不同的IO的具体实现,select模型会作为通用的网络IO模型作为兜底,为redis提供随时随地的可用性,虽然select机制因为1024 fd数目限制和轮询机制,使得性能比较不OK。</p><p>2.底层数据结构</p><blockquote>数据类型可以通过object encoding key查看</blockquote><p>2.1 sds简单动态字符串,<br>sds相较于C字符串具有内存安全、可直接获取长度、可保存二进制数据等优点,根据value的不同,分为intembstrraw encoding<br>2.2 linkedlist双端链表,每一个listNode都有prev/next指针指向相邻节点,双端链表、无环、多态<br>2.2 ziplist压缩列表,压缩列表是一个连续的内存块,能够高效利用内存顺序读写的优势,避免离散读的低效,ziplist的结构为:zlbytes|zltail|zllen|entries...|zlend,对于key.value类型的数据,key.value是连续存储的,所以他既可以用来做链表的数据结构也可以做zset何hashtable的数据结构<br>2.3 字典,又叫hashtable,里面有两个ht,一般存储的数据会放在ht[0]中,当负载因子大于5时进行扩容,扩容过程如下:</p><blockquote>1.在ht[1]申请一块空间,大小相当于ht[0]的2的幂次方<br>2.rehashidx置为0,表示开始扩容,<br>3.在curd进行的同时,rehashidx+1,数据从ht[0]迁移到ht[1]上,<br>4.等到ht[0]中的数据为空时,清空ht[0],rehashidx置为-1,ht[1]替代成为ht[0]</blockquote><p>2.4 skiplist,跳表,是由多层链表组成的数据结构,层数最多有32层,当存取数据时,决定从哪一层开始遍历,是靠投币决定的,定位到对应层级之后,开始查找链表中的位置,目标值较大则向右查找,较小则向左查找,在最底层,是完整的链表,存储了所有数据,</p><blockquote>skiplist相比于二叉平衡树而言,维持节点平衡的代价较小,只需要更改对应层级上链表节点指针即可,而二叉树则需要局部翻转。<br>skiplist并不只是用list存储数据,key和score映射关系是放在hashtable中的,排序用score排序,当定位到具体某一个Key时,就需要用hashtable<p>2.5 整数集合,intset,是集合的实现之一,intset有encoding/length和contents数组组成,encoding是只能升级不能降级的,添加新元素需要判断新元素类型的编码是否高于现有编码,如果需要升级的话,需要重新为旧元素分配新空间,然后将新元素追加到新空间中。</p></blockquote><p>2.6 quicklist,是利用ziplist压缩列表优势,避免linkedlist指针多占用空间和随机读写性能差的问题而产生的数据结构,结构如下,linkedlist的每一个节点存放ziplist,每一个ziplist用linkedlist双指针连接起来。</p><p>字符串:sds<br>list: ziplist、linkedlist、quicklist(ziplist+linkedlist)<br>hash: ziplist/dict<br>set: intset/hashtable<br>zset: ziplist/skiplist(hashmap)</p><p>3.其他数据结构:hyperlog基数统计、geo坐标分析(hash比对来确认是否就近),bitmap布隆过滤器,发布订阅</p><p>4.RDB与AOF<br>RDB是快照文件,是通过save命令或bgSave来对当前redis内存数据形成一个快照,配置一般为save m n,指m秒内n个命令,rdb需要fork子进程,在fork时,创建子进程会阻塞当前进程,子进程会将fork时的内存作为快照存储下来,在快照文件形成期间,如果此时有新的写请求会通过COW机制,在对应目标的内存块复制一个copy,然后在copy上面进行写入,当快照文件完成时,copy会替换原内存块。<br>bgrewriteaof前内存快照涉及命令的重新写入,出现此操作,除了手动操作之外,一般是aof文件过大,导致触达阈值而触发的,在bgrewrite期间,redis会对当前快照的key重新对应的命令,如lpush key1 val1 val2...,以64个为单位重写,对于已过期的key,则不会重新写入,大大节省了aof文件空间,而在此期间,redis还会新加一个rewiteaof_buf,用来存储rewrite期间的aof命令,当新aof文件完成后,rewriteaof_buf会刷入新文件中。<br>aof:对应aof_buf,是对写请求的日志记录,一般来讲,数据的写入是先写日志再写数据的,而redis则是,先写数据,再写文件,所以在aof期间如果aof未及时刷盘会有丢失数据的风险。aof刷盘策略有三种:<br>always:每一个写请求都会刷盘fsync<br>everysec每一秒调用fsync刷盘<br>no:刷盘动作交给操作系统write,周期刷盘,一般为30秒</p><p>5.redis-sentinel哨兵模式<br>哨兵负责监控redis集群的状态,自动进行故障转移,同步消息等。<br>sentinel故障转移过程:<br>当一个redismaster实例不能及时响应sentinel发来的心跳检测时,负责监控的sentinel会认为主观下线,并告知其他sentinel,其他sentinel会对这个实例再次检测,如果一般以上认为主观下线,则标记为客观下线,开始选主过程,sentinel会选择一个主sentinel(多数原则)然后由主sentinel从master的从节点中选择一个,选择的规则依次是:复制偏移量>最近响应>nodeID...(这里其实有很多),选出最优的slave成为新的master。</p><p>6.主从同步机制<br>master-slave架构下,slave需要主动发送sync给master,master收到之后会bgsave形成一个快照文件,将这个快照文件发送给slave,在此期间产生的命令aof_buf中的命令也会在快照之后发送给slave,slave根据快照和aof来回放,完成同步。</p><p>7.redis-cluster<br>slot槽是一个虚拟概念,并不实际存在,但是槽可以决定你的key分布在哪一台机器上,redis-cluster有16384个槽,通过crc16(key)%16384得出key所对应的存储节点,槽的范围0~16383,关于槽的分配,ruby可以自动分配槽到对应的节点中,可以使用range方法,也可以使用roundrobin方法,将槽与节点的关系映射起来。<br>槽分配之后,cluster中的每一个节点就拥有槽与管理者的全部映射关系,当接到key请求后,当前节点会首先查看本地的16384位的二进制序列为中对应位置是否是1,这个二进制序列位中,每一位对应着一个槽,如果是1的话,表示当前节点拥有该槽,如果没有,则节点还存储了全局的索引数组,这个索引数组反映的也是槽与位的映射关系,从这里找到对应节点然后进一步处理。<br>redis-cluster通信端口:在各自端口的基础上+10000<br>redis-cluster gossip协议:当新节点加入时,会发送meet给集群,正常情况下, 每一个节点会定时向最近未通信的节点随机选几个发送ping消息,发送完之后会收到响应pong,如果超时未响应,则会认为目标节点主观下线,当集群中超半数节点都认为该节点主观下线,那么该节点会进入选主流程。<br>选主流程:slave确认masterfail之后会带上自己的currentEpoch发起投票请求Failover_auth_request,在本epoch之内,其他master节点发出FAILOVER_AUTH_ACK投票,半数以上即让该slave升级为master,新master要做以下几件事情:1.从slave升级成为Master,2.delslot和addslot接管槽位,3.slave同步连接,广播自己选为master的消息。</p><p>8.缓存击穿/穿透/雪崩<br>击穿:当大量请求访问一个热点key时,热点key突然失效,此时会将请求量直接打到数据库上,导致数据库压力剧增。解决方案:热点key不过期策略,如果要刷新,只需要业务程序去更新缓存就可。<br>穿透:请求打到一个不存的的缓存key上,然后会将请求量打到数据库上,数据库压力陡增。解决方案:null缓存<br>雪崩:当大量缓存数据都在同一时刻过期,大量请求打在了数据库上,数据库压力陡增。解决方案:缓存key过期时间随机。</p><p>9.读写分离和本地缓存<br>redis-cluster或其他的集群方案,其中有一种可能性是,请求量都会被打到某一个热点key所在的节点上,而且这种方式用redis-cluster扩容没办法解决,单机上最多10W个QPS可能没办法承受对单个key的访问,这时候就需要有能够将读请求和写请求分离的设计方案了,这种方案需要设置代理层,代理层主要做两件事:路由转发设置读写分离、热点数据缓存,关于热点数据缓存,需要有一个算法能够将哪些是热点哪些不是热点,有选择地缓存热点数据到proxy上,承担一部分读请求压力。<br>本地缓存,如果redis实例压力太大,需要借助应用服务的本地缓存来实现限流,防止所有流量都达到redis上,但是这种做法让redis实例更新数据时无法及时更改到本地,本地缓存则需要加上一个合理的过期时间,防止脏数据存留太久。</p><p>10.分布式锁</p><ul><li>一般来讲,使用redis实现的分布式锁,应该有以下几个步骤:<br>10.1 自旋等待,并查看锁是否释放(下一步)<br>10.2 setnx或set(key, value, expireTime, ['NX'])得到锁<br>10.3 处理事务<br>10.4 查看锁是否过期(检查锁是否仍然是自己拥有),CAS<br>10.5 如果锁仍然自持,那么释放锁,如果锁已经过期,回滚事务或者报错<br>所以分布式锁的过期时间和事务耗时很关键。</li><li>升级一点:如果单节点的redis分布式锁存在以下情况:加锁的redis节点宕机了,怎么办?<br> 对于这种情况,redis可以通过自动故障转移和数据持久化尽量减少并发锁问题,但是仍然不能解决极端状况下redis锁丢失或者同时出现两个锁的问题。<br> Redis的作者有一个思路,叫做redlock,大致实现过程如下:<br> 假设现在集群中有ABCDE个节点,客户端加锁的请求,要同时向这5个节点发起,这5个节点要有半数以上都可以获得这个锁,加锁的value,是客户点的毫秒过期时间,响应回客户端时,客户端需要去校验现在的时间是否已过期,如果已经过期,那么加锁失败,如果半数以上都在有效期之内,那么加锁成功。<br> 假设如果其中有一台机器宕机不工作了,未来得及持久化又丢掉了数据,然后又重启,怎么办呢?这里可以设置重启时间要在过期时间间隔之后再操作,这样,只要锁过期了,就跟redis没什么关系了。</li></ul><p>11.事务和管道<br>关于事务的几个命令:<br>watch:会监控操作的key是否会被其他的事务操作,如果有,那么直接return,下面的事务不会执行<br>MUlti: 开始事务,相当于数据库的begin transaction<br>exec: 提交事务,相当于数据库的commit动作<br>discard: 丢弃,以上事务的动作不会执行,所有命令会被丢弃掉,可类比于rollback。</p><p>管道:管道是批量发送命令的操作,它可以将多个命令打包起来发给redis服务器,然后批量返回响应。这样有利于减少与redis之间的网路传输消耗。</p><p>12.内存淘汰策略和过期策略<br>no-eviction 直接拒绝写入<br>allkeys-lru 所有键里的LRU淘汰<br>volentile-lru 过期key里的LRU淘汰<br>allkeys-random 所有key里的随机淘汰<br>volentile-random 过期key里的随机淘汰<br>allkeys-lfu 所有key里的最近访问频率<br>volentile-lfu 所有key里的最近访问频率</p><ul><li>LRU算法:redis的LRU算法其实是近似LRU算法,使用的是随机采样方式,每次随机采样5个key,选择这5个key中的最久的Key,与LRU池子里的最早的key比较,如果比这个key还早,那么放进去这个池子,这样到最后,这个池子就会满,满了的时候,这个池子中的最早的key会被直接淘汰掉,这是一种性能和空间权衡之后的结果</li></ul><p>过期策略<br>定时,需要有一个计时器,当key到期之后,就会执行删除,对CPU来说有多余的计算量,需要维护计时器监听key是否过期,但是对内存来说是很友好的,可以及时清理掉不必要的数据。<br>定期,每隔一段时间清理掉那些过期的key,对CPU和内存都好一些<br>惰性,每次请求来的时候才判断key是否过期,对CPU友好,对内存不友好,因为会占用一段时间的内存。</p><p>其他数据结构:</p><p>hyperlog:基数统计<br>bitmap:布隆过滤器<br>geo:经纬度,可以依靠经纬度hash来计算近似和距离<br>发布订阅:发布订阅是将消息传递给订阅链表上的所有client,支持两种模式,一种是模式匹配,另一种是完全模式</p>
Go语言实现一个区块链
https://segmentfault.com/a/1190000019793017
2019-07-17T19:18:28+08:00
2019-07-17T19:18:28+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
3
<p>本文将逐步拆解实现区块链功能的几个步骤</p>
<h2>你需要掌握的基本知识:</h2>
<ul>
<li>什么是区块链</li>
<li>sha256哈希加密算法</li>
<li>go语言基础,包括goroutine和channel的理解</li>
</ul>
<h3>准备工作</h3>
<ul>
<li>go get github.com/davecgh/go-spew/spew spew是一个非常好的打印输出工具,可以在终端输出struct和slice数据</li>
<li>go get github.com/gorilla/mux mux可以用来处理http请求,帮助我们快速搭建一个go服务器</li>
<li>go get github.com/joho/godotenv 这个包可以读取.env文件中的变量</li>
</ul>
<blockquote>.env文件需要在项目的根目录下,一般在main.go所在位置</blockquote>
<ul><li>一款给力的IDE,比如Goland</li></ul>
<h3>几个概念</h3>
<ul>
<li>挖矿,挖矿其实就是通过解决一类数学难题,得到在现有区块链上创建一个区块的权利,并获得一些奖励,比如比特币,以太币等。</li>
<li>PoW(Proof of work),简单来说PoW就是:有一个Nonce值(值随意),这个Nonce值和区块的数据结合在一起通过SHA256得到一个哈希值,如果这个哈希值的前N(difficulty)位字符都是0,那么就算解决了这个数学难题,可以创建一个区块。</li>
<li>区块链,区块链是前后紧密连接的,每一个Block都会记录上一个区块的哈希,如果当前生成的区块的所记录的PrevHash与上一区块不同的话,那么此次生成就无效,同理,如果任何一个人想在区块链的某一个区块上修改数据,那么就会造成整个链无效。</li>
</ul>
<h3>创建项目</h3>
<ul>
<li>在$GOPATH的src下创建项目blockChain</li>
<li>在blockChain下创建文件.env和main.go</li>
</ul>
<p>在.env文件中写入PORT=8088</p>
<h3>需要引入的包</h3>
<pre><code class="go">import (
"crypto/sha256"
"encoding/hex"
"time"
"os"
"log"
"net/http"
"github.com/gorilla/mux"
"encoding/json"
"io"
"github.com/davecgh/go-spew/spew"
"sync"
"strconv"
"strings"
"fmt"
"github.com/joho/godotenv"
"net"
"bufio"
)</code></pre>
<h2>区块逻辑</h2>
<h3>定义Block区块结构体</h3>
<pre><code class="go">type Block struct {
Index int // 表示区块所在区块链的位置
Timestamp string // 生成区块的时间戳
Data int // 写入区块的数据
Hash string // 整个区块数据SHA256的哈希
PrevHash string // 上一个区块的哈希值
Difficulty int // 定义难度
Nonce string // 定义一个Nonce
}</code></pre>
<h3>定义常量和一些变量</h3>
<pre><code class="go">const difficulty = 1 // 定义难度,也就是哈希包含多少个0的前缀
var mutex = &sync.Mutex{} // 防止并发写入请求造成的错误,加入互斥锁
var BlockChain []Block // 定义一个区块链,数据元素要全部都是Block
var bcServer chan []Block // 定义一个channel,处理各个节点之间的同步问题</code></pre>
<h3>计算区块哈希</h3>
<pre><code class="go">/**
计算区块哈希值
*/
func calculateHash(block Block) string {
record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash + block.Nonce // 得到当前block区块的字符串拼接,按照索引、时间戳、所含数据、上一个区块哈希来进行记录,Nonce值一并加入
h := sha256.New() // 得到sha256哈希算法
h.Write([]byte(record)) // 得到对应哈希
hashed := h.Sum(nil)
return hex.EncodeToString(hashed) //转化为字符串返回
}</code></pre>
<h3>生成新的区块</h3>
<pre><code class="go">/**
生成一个区块,根据上一个区块
*/
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1 // 索引自增
newBlock.Timestamp = t.String() // 时间戳
newBlock.Data = Data // 数据
newBlock.PrevHash = oldBlock.Hash // 上一个区块的哈希
newBlock.Difficulty = difficulty // 难度
//newBlock.Hash = calculateHash(newBlock) // 计算本区块的哈希
for i := 0; ; i++ {
hex := fmt.Sprintf("%x", i) // 16进制展示
newBlock.Nonce = hex
newHash := calculateHash(newBlock) // 计算哈希
if !isHashValid(newHash, newBlock.Difficulty) {
fmt.Println(newHash, " 继续努力!?")
time.Sleep(time.Second) // 每隔1s执行一次
continue
} else {
fmt.Println(newHash, " 已经成功!")
newBlock.Hash = newHash
break
}
}
return newBlock, nil
}</code></pre>
<h3>验证区块是否合法</h3>
<pre><code class="go">/**
验证区块是否合法
*/
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index + 1 != newBlock.Index { // 如果索引不继承自上一个,验证不通过
return false
}
if oldBlock.Hash != newBlock.PrevHash { // 如果哈希不继承上一个区块,验证不通过
return false
}
if calculateHash(newBlock) != newBlock.Hash { // 如果计算出来的哈希不一致,验证不通过
return false
}
return true
}</code></pre>
<h3>验证哈希是否符合PoW</h3>
<pre><code class="go">/**
验证哈希的前缀是否包含difficulty个0
*/
func isHashValid(hash string, difficulty int) bool {
prefix := strings.Repeat("0", difficulty)
return strings.HasPrefix(hash, prefix)
}</code></pre>
<h3>选择长链</h3>
<blockquote>因为在实际场景中,区块链可能会产生分叉,造成A和B长短不一的情况,故而选择长的作为新链</blockquote>
<pre><code class="go">/**
选择长链作为正确的链
*/
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(BlockChain) { // 计算数组长度
BlockChain = newBlocks
}
}</code></pre>
<h2>同步节点逻辑</h2>
<p><img src="/img/bVbvddv?w=677&h=914" alt="图片描述" title="图片描述"><br>如图所示,节点数据同步就是通过新节点中生成一个区块后,先通过channel传递给主线程,然后主线程广播给各个节点来完成的。</p>
<h3>监听连接逻辑</h3>
<pre><code class="go">/**
处理连接
*/
func handleConn(conn net.Conn) {
defer conn.Close() // 完成后关闭
spew.Dump(conn)
io.WriteString(conn, "输入数字:")
scanner := bufio.NewScanner(conn)
go func() {
for scanner.Scan() { // 轮询扫描所有tcp连接
data, err := strconv.Atoi(scanner.Text())
if err != nil {
log.Printf("%v 非数字", scanner.Text(), err)
}
newBlock, err := generateBlock(BlockChain[len(BlockChain) - 1], data)
if err != nil {
log.Println(err)
continue
}
if isBlockValid(newBlock, BlockChain[len(BlockChain) - 1]) {
newBlockChain := append(BlockChain, newBlock)
replaceChain(newBlockChain)
}
bcServer <- BlockChain // 将生成的区块数据交给通道,单向传递
io.WriteString(conn, "\n输入数字:")
}
}()
go func() {
for { // 每隔10s同步一次
time.Sleep(10 * time.Second)
output, err := json.MarshalIndent(BlockChain, "", " ")
if err != nil {
log.Fatal(err)
}
io.WriteString(conn, "\n↓↓↓↓↓↓↓↓↓↓↓↓↓ 同步区块链:↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n"+ string(output) + "\n↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑\n")
}
}()
for _= range bcServer {
spew.Dump(BlockChain)
}
}
</code></pre>
<h3>主函数</h3>
<pre><code class="go">func main () {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
bcServer = make(chan []Block) // 创建通道
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", "", difficulty, ""}
spew.Dump(genesisBlock)
BlockChain = append(BlockChain, genesisBlock) // 创世区块
server, err := net.Listen("tcp", ":" + os.Getenv("PORT")) // 监听TCP端口
if err != nil {
log.Fatal(err)
}
defer server.Close() // 完成后关闭server
for {
conn, err := server.Accept()
if err != nil {
log.Fatal()
}
go handleConn(conn) // 协程处理连接
}
}</code></pre>
<h3>全量代码</h3>
<pre><code class="go">package main
import (
"crypto/sha256"
"encoding/hex"
"time"
"os"
"log"
"net/http"
"github.com/gorilla/mux"
"encoding/json"
"io"
"github.com/davecgh/go-spew/spew"
"sync"
"strconv"
"strings"
"fmt"
"github.com/joho/godotenv"
"net"
"bufio"
)
//////////////////// 处理区块链 ////////////////////
const difficulty = 1 // 定义难度,也就是哈希包含多少个0的前缀
type Block struct {
Index int // 表示区块所在区块链的位置
Timestamp string // 生成区块的时间戳
Data int // 写入区块的数据
Hash string // 整个区块数据SHA256的哈希
PrevHash string // 上一个区块的哈希值
Difficulty int // 定义难度
Nonce string // 定义一个Nonce
}
var mutex = &sync.Mutex{} // 防止并发写入请求造成的错误,加入互斥锁
var BlockChain []Block // 定义一个区块链,数据元素要全部都是Block
var bcServer chan []Block // 定义一个channel,处理各个节点之间的同步问题
/**
计算区块哈希值
*/
func calculateHash(block Block) string {
record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash + block.Nonce // 得到当前block区块的字符串拼接,按照索引、时间戳、所含数据、上一个区块哈希来进行记录,Nonce值一并加入
h := sha256.New() // 得到sha256哈希算法
h.Write([]byte(record)) // 得到对应哈希
hashed := h.Sum(nil)
return hex.EncodeToString(hashed) //转化为字符串返回
}
/**
生成一个区块,根据上一个区块
*/
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1 // 索引自增
newBlock.Timestamp = t.String() // 时间戳
newBlock.Data = Data // 数据
newBlock.PrevHash = oldBlock.Hash // 上一个区块的哈希
newBlock.Difficulty = difficulty // 难度
//newBlock.Hash = calculateHash(newBlock) // 计算本区块的哈希
for i := 0; ; i++ {
hex := fmt.Sprintf("%x", i) // 16进制展示
newBlock.Nonce = hex
newHash := calculateHash(newBlock) // 计算哈希
if !isHashValid(newHash, newBlock.Difficulty) {
fmt.Println(newHash, " 继续努力!?")
time.Sleep(time.Second) // 每隔1s执行一次
continue
} else {
fmt.Println(newHash, " 已经成功!")
newBlock.Hash = newHash
break
}
}
return newBlock, nil
}
/**
验证区块是否合法
*/
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index + 1 != newBlock.Index { // 如果索引不继承自上一个,验证不通过
return false
}
if oldBlock.Hash != newBlock.PrevHash { // 如果哈希不继承上一个区块,验证不通过
return false
}
if calculateHash(newBlock) != newBlock.Hash { // 如果计算出来的哈希不一致,验证不通过
return false
}
return true
}
/**
验证哈希的前缀是否包含difficulty个0
*/
func isHashValid(hash string, difficulty int) bool {
prefix := strings.Repeat("0", difficulty)
return strings.HasPrefix(hash, prefix)
}
/**
选择长链作为正确的链
*/
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(BlockChain) { // 计算数组长度
BlockChain = newBlocks
}
}
////////////////// 主函数 /////////////////
func main () {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
bcServer = make(chan []Block) // 创建通道
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", "", difficulty, ""}
spew.Dump(genesisBlock)
BlockChain = append(BlockChain, genesisBlock) // 创世区块
server, err := net.Listen("tcp", ":" + os.Getenv("PORT")) // 监听TCP端口
if err != nil {
log.Fatal(err)
}
defer server.Close() // 完成后关闭server
for {
conn, err := server.Accept()
if err != nil {
log.Fatal()
}
go handleConn(conn) // 协程处理连接
}
}
/**
处理连接
*/
func handleConn(conn net.Conn) {
defer conn.Close() // 完成后关闭
spew.Dump(conn)
io.WriteString(conn, "输入数字:")
scanner := bufio.NewScanner(conn)
go func() {
for scanner.Scan() { // 轮询扫描所有tcp连接
data, err := strconv.Atoi(scanner.Text())
if err != nil {
log.Printf("%v 非数字", scanner.Text(), err)
}
newBlock, err := generateBlock(BlockChain[len(BlockChain) - 1], data)
if err != nil {
log.Println(err)
continue
}
if isBlockValid(newBlock, BlockChain[len(BlockChain) - 1]) {
newBlockChain := append(BlockChain, newBlock)
replaceChain(newBlockChain)
}
bcServer <- BlockChain // 将生成的区块数据交给通道,单向传递
io.WriteString(conn, "\n输入数字:")
}
}()
go func() {
for { // 每隔10s同步一次
time.Sleep(10 * time.Second)
output, err := json.MarshalIndent(BlockChain, "", " ")
if err != nil {
log.Fatal(err)
}
io.WriteString(conn, "\n↓↓↓↓↓↓↓↓↓↓↓↓↓ 同步区块链:↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n"+ string(output) + "\n↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑\n")
}
}()
for _= range bcServer {
spew.Dump(BlockChain)
}
}</code></pre>
<h2>运行</h2>
<ul>
<li>打开终端,运行go run main.go,作为主线程终端</li>
<li>新开两个终端作为节点,运行 nc localhost 8088 或 telnet localhost 8088,输入相应的数字</li>
<li>等待生成区块,主线程显示如下</li>
</ul>
<p><img src="/img/bVbvdee?w=1410&h=626" alt="clipboard.png" title="clipboard.png"></p>
<ul><li>各个节点每过10s会接收主线程的同步区块链数据</li></ul>
<p><img src="/img/bVbvdeh?w=1266&h=1534" alt="clipboard.png" title="clipboard.png"></p>
<ul><li>你可以更换difficulty常量的值为2或3,计算时间会成倍增加。</li></ul>
<blockquote>以上节点间的广播同步是通过tcp连接来实现的,但更好的方案应该是p2p网络,需要安装libp2p包,这里不做赘述。</blockquote>
浅析分布式事务中的2PC和3PC
https://segmentfault.com/a/1190000019737435
2019-07-12T00:26:23+08:00
2019-07-12T00:26:23+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
1
<h2>分布式事务</h2>
<p>大型的分布式系统架构都会涉及到事务的分布式处理的问题,基本来说,分布式事务和普通事务都有一个共同原则:</p>
<ul>
<li>A(Atomic) 原子性,事务要么一起完成,要么一起回滚</li>
<li>C(Consistent) 一致性,提交数据前后数据库的完整性不变</li>
<li>I(Isolation) 隔离性,不同的事务相互独立,不互相影响</li>
<li>D(Duration)持久性,数据状态是永久的</li>
</ul>
<p>另外一个设计原则,分布式系统要符合以下原则:</p>
<ul>
<li>C(Consistent)一致性,分布式系统中存在的多个副本,在数据和内容上要一模一样</li>
<li>A(Availability)高可用性,在有限时间内保证系统响应</li>
<li>P(Partition tolerance)分区容错性,一个副本或一个分区出现宕机或其他错误时,不影响系统整体运行</li>
</ul>
<p>由于在设计分布式系统时,难以保证CAP全部符合,最多保证其中两个,例如在一个分布式系统中:<br>要保证系统数据库的强一致性,需要跨表跨库占用数据库资源,在复杂度比较高的情况下,耗时难以保证,就会出现可用性无法保证的情况。</p>
<p>故而又出现了BASE理论</p>
<ul>
<li>Basic Available基本可用,分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用</li>
<li>Soft state柔性状态,允许系统存在中间状态,这个状态不影响系统可用性</li>
<li>Eventually Consistent 最终一致性 指经过一段时间之后,所有数据状态保持一致的结果</li>
</ul>
<p>实际上BASE理论是基于AP的一个扩展,一个分布式事务不需要强一致性,只需要达到一个最终一致性就可以了。</p>
<p>分布式事务就是处理分布式中各个节点的数据操作,使之能够在节点相互隔离的情况下完成多个节点数据的一致性操作。</p>
<h3>分步式事务 - 2PC</h3>
<p>2PC即二阶段提交,在这里需要了解两个概念</p>
<ul>
<li>参与者:参与者即各个实际参与事务的节点,这些节点相互隔离,彼此不知道对方是否提交事务。</li>
<li>协调者:收集和管理参与者的事务信息,负责统一管理节点信息,也就是分布式事务中的第三方。</li>
</ul>
<h4>准备阶段,也称投票阶段</h4>
<p>准备阶段中,协调者向参与者发送precommit的消息,参与者在本地执行事务逻辑,如果没有超时以及运行无误,那么会记录redo和undo日志,将ack信息发送给协调者,当协调者收到所有节点参与者发送的ack信息时,准备进入下一阶段,否则会发送回滚信息给各个节点,各个节点根据之前记录好的undo信息回滚,本次事务提交失败。</p>
<h4>提交阶段</h4>
<p>提交阶段中,协调者已经收到所有节点的应答信息,接下来发送commit消息给各个节点,通知各个节点参与者可以提交事务,各个参与者提交完毕后一一发送完成信息给协调者,并释放本地占用的资源,协调者收到所有完成消息后,完成事务。</p>
<p>二阶段提交的图示如下:<br><img src="/img/bVbuYEp?w=505&h=305" alt="图片描述" title="图片描述"></p>
<h4>二阶段提交的问题</h4>
<p>二阶段提交虽然能够解决大多数的分布式事务的问题,且发生数据错误的概率极小,但仍然有以下几个问题:</p>
<ul>
<li>单点故障:如果协调者宕机,那么参与者会一直阻塞下去,如果在参与者收到提交消息之前协调者宕机,那么参与者一直保持未成功提交的状态,会一直占用资源,导致同时间其他事务可能要一直等待下去。</li>
<li>同步阻塞问题:因为参与者是在本地处理的事务,是阻塞型的,在最终commit之前,一直占用锁资源,如果遇到时间较长而协调者未发回commit的情况,那么会导致锁等待。</li>
<li>数据一致性问题:假如协调者在第二阶段中,发送commit给某些参与者失败,那么就导致某些参与者提交了事务,而某些没有提交,这就导致了数据不一致。</li>
<li>二阶段根本无法解决的问题:假如协调者宕机的同时,最后一个参与者也宕机了,当协调者通过重新选举再参与到事务管理中时,它是没有办法知道事务执行到哪一步的。</li>
</ul>
<p>针对于以上提出的一些问题,衍生出了3PC。</p>
<h3>分布式事务 - 3PC</h3>
<p>3PC即三阶段提交协议。</p>
<p><img src="/img/bVbuYKs?w=323&h=180" alt="图片描述" title="图片描述"></p>
<p>3PC相对于2PC,有了以下变化:</p>
<ul>
<li>引入超时机制,在参与者和协调者中都加入了各自的超时策略。</li>
<li>加入了一个新的阶段,canCommit,所以阶段分成了三个:canCommit、PreCommit、DoCommit。</li>
</ul>
<h4>CanCommit</h4>
<p>这是一个准备阶段,在这一阶段中,协调者向参与者发送CanCommit消息,参与者接收后,根据自身资源情况判断是否可以执行事务操作,如果可以并且未超时,则发送yes给协调者,反之,协调者会中断事务。</p>
<h4>PreCommit</h4>
<p>这是预备阶段,在这一阶段中,协调者向参与者发送PreCommit消息,参与者接收后,会执行事务操作,记录undo和redo日志,事务执行完毕后会将Ack消息发送给协调者,协调者在未超时的情况下收集所有参与者的信息,否则中断事务,通知所有参与者Abort消息。</p>
<h4>DoCommit</h4>
<p>当所有参与者Ack消息完毕之后,协调者会确认发送DoCommit消息给每一个参与者,执行提交事务。原则上所有参与者执行提交完毕之后,需要发送Committed给协调者,协调者完成事务。否则协调者中断事务,参与者接收abort消息会根据之前记录的undo回滚。<br>但也要注意,此阶段中如果参与者无法及时收到协调者发来的Docommit消息时,也会自行提交事务,因为从概率上来讲,PreCommit这个阶段能够不被abort说明全部节点都可以正常执行事务提交,所以一般来讲单个节点提交不影响数据一致性,除非极端情况。</p>
<h3>2PC 和 3PC的区别</h3>
<p>相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。</p>
<h3>消息中间件</h3>
<p>上面提到的关于协调者在实践过程中由谁来担任是个问题,一般来讲这个协调者是异步的,可以在参与者和协调者之间双向通信的,有良好的消息传递机制,能够保证消息稳定投递。故而一般协调者的担任者是高性能的消息中间件例如RocketMq、Kafka、RabbitMq等。</p>
<p>如图就是一个2PC的实践:<br><img src="/img/bVbuYLF?w=1544&h=988" alt="图片描述" title="图片描述"></p>
Nuxtjs服务端渲染实践,搭建一个blog
https://segmentfault.com/a/1190000019732001
2019-07-11T15:10:48+08:00
2019-07-11T15:10:48+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
1
<h2>关于SSR的简介</h2>
<p>SSR,即服务端渲染,这其实是旧事重提的一个概念,我们常见的服务端渲染,一般见于后端语言生成的一段前端脚本,如:php后端生成html+jsscript内容传递给浏览器展现,nodejs在后端生成页面模板供浏览器呈现,java生成jsp等等。</p>
<p>Vuejs、Reactjs、AngularJs这些js框架,原本都是开发web单页应用(SPA)的,单页应用的好处就是只需要初次加载完所有静态资源便可在本地运行,此后页面渲染都只在本地发生,只有获取后端数据才需要发起新的请求到后端服务器;且因为单页应用是纯js编写,运行较为流畅,体验也稍好,故而和本地原生应用结合很紧密,有些对页面响应流畅度要求不是特别苛刻的页面,用js写便可,大大降低了app开发成本。</p>
<p>然而单页应用并不支持良好的SEO,因为对于搜索引擎的爬虫而言,抓取的单页应用页面源码基本上没有什么变化,所以会认为这个应用只有一个页面,试想一下,一个博客网站,如果所有文章被搜索引擎认为只有一个页面,那么你辛辛苦苦写的大多数文章都不会被收录在里面的。</p>
<p>SSR首先解决的就是这个问题,让人既能使用Vuejs、Reactjs来进行开发,又能保证有良好的SEO,且技术路线基本都是属于前端开发栈序列,语言语法没有多大变化,而搭载在Nodejs服务器上的服务端渲染又可以有效提高并发性能,一举多得,何乐而不为?</p>
<blockquote>ps:当然,目前某些比较先进的搜索引擎爬虫已经支持抓取单页应用页面了,比如谷歌。但并不意味着SSR就没用了,针对于资源安全性要求比较高的场景,搭载在服务器上的SSR有着天然的优势。</blockquote>
<h2>关于Nuxtjs</h2>
<p><a href="https://link.segmentfault.com/?enc=srgf2qmEmSZXzM0%2Bfuyv0g%3D%3D.v2urvcxJh8Es2vhJ7c4E9wvi87mjbvjy3Ke7Xl2yMRc%3D" rel="nofollow">这里</a>是官方介绍,Nuxtjs是诞生于社区的一套SSR解决方案,是一个比较完备的Vuejs服务端渲染框架,包含了异步数据加载、中间件支持、布局支持等功能。</p>
<p>关于nuxtjs,你必须要掌握以下几点知识:</p>
<ol>
<li>vuejs、vue-router、vuex等</li>
<li>nodejs编程</li>
<li>webpack构建前端工程</li>
<li>babel-loader</li>
</ol>
<blockquote>如果想使用进程管理工具,推荐使用pm2管理nodejs进程,安装方式为:<code>npm install -g pm2</code>
</blockquote>
<h2>搭建一个blog</h2>
<h3>准备好工具</h3>
<p><a href="https://link.segmentfault.com/?enc=YDjZGMwKNMj1c55INRICzw%3D%3D.8PNXY9UAFfFvbMzp8%2FeibrM9JHWqIMejDGqGYKicHkDD2YMWL%2Brcmh6Czl8S2r3W" rel="nofollow">推荐下载</a></p>
<p>这里iview将作为一个插件在nuxtjs项目中使用。</p>
<p>注意几个配置:<br>nux.config.js</p>
<pre><code class="js">module.exports = {
/*
** Headers of the page
*/
head: {
title: '{{ name }}',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '{{escape description }}' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
plugins: [
{src: '~plugins/iview', ssr: true}
],
/*
** Customize the progress bar color
*/
loading: { color: '#3B8070' },
/*
** Build configuration
*/
build: {
/*
** Run ESLint on save
*/
extend (config, { isDev, isClient }) {
if (isDev && isClient) {
config.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/
})
}
}
}
}</code></pre>
<p>plugins文件夹下,加入iview.js</p>
<pre><code class="js">import Vue from 'vue';
import iView from 'iview';
Vue.use(iView);
import 'iview/dist/styles/iview.css';</code></pre>
<p>如果你想要加入其它的配置,可以在nuxt.config.js的plugins配置项中加入,同时在plugins文件夹下加入引入逻辑。例如:<br>nuxt.config.js</p>
<pre><code class="js">{src: '~plugins/vuetify', ssr: true}</code></pre>
<p>plugins/vuetify.js</p>
<pre><code class="js">import Vue from 'vue'
import Vuetify from 'vuetify'
Vue.use(Vuetify)
import 'vuetify/dist/vuetify.min.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'</code></pre>
<p>配置很方便。</p>
<h3>开始写页面</h3>
<p>页面布局</p>
<pre><code class="vue"><template>
<div data-app>
<v-app>
<!-- header -->
<v-toolbar dark color="primary" fixed v-show="showToolbar">
<!--<v-toolbar-side-icon @click="drawer = !drawer"></v-toolbar-side-icon>-->
<v-toolbar-title class="white--text"><a href="/" style="text-decoration-line:none;line-height: 40px;height: 40px;font-size:1.2em;color:white;">&nbsp;Blog</a></v-toolbar-title>
<v-spacer></v-spacer>
<v-spacer></v-spacer>
<v-spacer></v-spacer>
<!--<v-btn icon @click="showSearch">-->
<!--<v-icon>search</v-icon>-->
<!--</v-btn>-->
<!--<v-text-field-->
<!--hide-details-->
<!--prepend-icon="search"-->
<!--single-line-->
<!--clearable-->
<!--color="yellow"-->
<!--placeholder="输入博客内容"-->
<!--&gt;</v-text-field>-->
<v-autocomplete
v-model="searching"
:items="articles"
item-text="name"
item-value="id"
color="red"
prepend-icon="search"
placeholder="输入搜索内容"
hide-no-data
hide-selected
:loading="isLoading"
browser-autocomplete
clearable
:search-input.sync="changeSearch"
>
<template slot="selection" slot-scope="data">
{{data.item.name}}
</template>
<template slot="item" slot-scope="data">
<v-list-tile-content @click="toDetail(data.item.id)">
<v-list-tile-title v-html="data.item.name"></v-list-tile-title>
<v-list-tile-sub-title v-html="data.item.group"></v-list-tile-sub-title>
</v-list-tile-content>
</template>
</v-autocomplete>
<v-menu bottom transition="slide-y-transition" offset-y open-on-hover left>
<v-btn icon
slot="activator"
>
<img :src="languageChoice" alt="" width="26px">
</v-btn>
<v-list>
<v-list-tile @click="languageChoice = '/imgs/cn.webp'">
<img src="/imgs/cn.webp" alt="">&nbsp; <v-list-tile-title>简体中文</v-list-tile-title>
</v-list-tile>
<v-list-tile @click="languageChoice = '/imgs/us.webp'">
<img src="/imgs/us.webp" alt="">&nbsp;<v-list-tile-title>English</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
<v-tooltip bottom >
<v-btn icon slot="activator" href="mailto:thundervsflash@qq.com" nuxt>
<v-icon>contact_mail</v-icon>
</v-btn>
<span>mailto:thundervsflash@qq.com</span>
</v-tooltip>
<v-menu bottom left transition="slide-y-transition" offset-y open-on-hover>
<v-btn
slot="activator"
dark
icon
>
<v-icon>more_vert</v-icon>
</v-btn>
<!--<v-list style="width:150px">-->
<!--<v-list-tile-->
<!--href="/about"-->
<!--target="_blank"-->
<!--&gt;-->
<!--<v-avatar size="30px" color="lime">-->
<!--<v-icon dark small>account_circle</v-icon>-->
<!--</v-avatar>-->
<!--<v-spacer></v-spacer>-->
<!--<v-list-tile-title style="text-align: end"><span style="margin-right:10px;">关于我</span></v-list-tile-title>-->
<!--</v-list-tile>-->
<!--</v-list>-->
</v-menu>
</v-toolbar>
<!-- content -->
<v-content :style="contentStyle">
<nuxt/>
</v-content>
<v-btn
fab
color="red"
bottom
right
style="bottom:20%"
fixed
@click="toAdd"
>
<v-icon color="white">add</v-icon>
</v-btn>
<!-- footer -->
<v-footer style="margin-top:25px;">
<v-layout
justify-center
row
wrap
>
<v-flex xs12 text-xs-center indigo darken-4 white--text py-2>
Site's built by <a href="https://vuejs.org">vuejs</a>/<a href="https://vuetifyjs.com">vuetifyjs</a>/<a
href="https://nuxtjs.org">nuxtjs</a>/<a href="https://lumen.laravel.com">lumen</a>/<a href="https://github.com/hhxsv5/laravel-s">laravel-swoole</a>/<a
href="https://wiki.swoole.com/">swoole</a> etc.
</v-flex>
<v-flex
indigo darken-4
py-3
text-xs-center
white--text
xs12
>
&copy;2017-{{(new Date()).getFullYear()}}&nbsp;<strong><a href="/">Rainbow-blog</a> by Henry. All rights reserved.</strong>
</v-flex>
</v-layout>
</v-footer>
<!-- back to top -->
<v-fab-transition>
<v-btn
v-show="showUp"
color="red"
v-model="fab"
dark
fab
fixed
bottom
right
@click="$vuetify.goTo(target, options)"
>
<v-icon>keyboard_arrow_up</v-icon>
</v-btn>
</v-fab-transition>
<v-snackbar
v-model="snackbar"
color="info"
:timeout="3000"
:vertical="true"
top
right
>
{{ location }}
<v-btn
dark
flat
@click="snackbar = false"
>
Close
</v-btn>
</v-snackbar>
</v-app>
</div>
</template>
<script>
export default {
head: {
},
data() {
return {
location: "",
snackbar: false,
languageChoice: "/imgs/cn.webp",
contentStyle: {
marginTop:"64px"
},
showToolbar: true,
drawer: null,
items: [
{ title: 'Home', icon: 'dashboard' },
{ title: 'About', icon: 'question_answer' }
],
mini: false,
right: null,
showUp: false,
fab: true,
target: 0,
options: {
duration: 300,
offset: 0,
easing: 'easeInOutCubic'
},
articles: [
],
searching: "",
isLoading: false,
changeSearch: ""
}
},
watch: {
changeSearch(newV, oldV) {
if (newV == 'undefined' || !newV) {
return ;
}
this.isLoading = true
// Lazily load input items
this.$axios.get('https://api.hhhhhhhhhh.com/blog/index?'+ '_kw='+newV)
.then(res => {
this.articles = res.data
console.log(this.articles);
})
.catch(err => {
console.log(err)
})
.finally(() => (this.isLoading = false))
console.log(this.articles);
},
languageChoice(value) {
this.$axios.post('https://api.hhhhhhhh.com/hhhhh').then(res => {
this.snackbar = true;
this.location = res.data.location;
});
}
},
mounted() {
window.addEventListener('scroll', () => {
if (window.pageYOffset > 80) {
this.showUp = true;
if (this.$route.fullPath == '/') {
this.showToolbar = true;
}
} else {
this.showUp = false;
if (this.$route.fullPath == '/') {
this.showToolbar = false;
}
}
});
if (this.$route.fullPath == '/') {
this.contentStyle.marginTop = "0px";
this.showToolbar = false;
}
},
methods: {
toAdd() {
location.href = '/hhhhh'
},
getHighlight(originStr) {
if (!this.searching) {
return originStr;
}
let ind = originStr.indexOf(this.searching);
let len = this.searching.length;
return originStr.substr(0, ind) + "<code>" + this.searching + "</code>" + originStr.substr(ind + len);
},
toDetail(id) {
location.href = "/blog/"+id;
},
}
}
</script>
<style>
html {
font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 16px;
word-spacing: 1px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: border-box;
margin: 0;
}
.button--green {
display: inline-block;
border-radius: 4px;
border: 1px solid #3b8070;
color: #3b8070;
text-decoration: none;
padding: 10px 30px;
}
.button--green:hover {
color: #fff;
background-color: #3b8070;
}
.button--grey {
display: inline-block;
border-radius: 4px;
border: 1px solid #35495e;
color: #35495e;
text-decoration: none;
padding: 10px 30px;
margin-left: 15px;
}
.button--grey:hover {
color: #fff;
background-color: #35495e;
}
div.v-image__image--cover {
filter: blur(5px) !important;
}
.v-btn--floating .v-icon {
height: auto !important;
}
code {
box-shadow: none !important;
-webkit-box-shadow: none !important;
}
</style>
</code></pre>
<p>所有页面都写在page/文件夹之下,例如新建一个index.vue页面</p>
<pre><code class="vue"><template>
<div>
<v-parallax
src="./bg2.jpg"
:height="bgHeight"
>
<v-layout
align-center
column
justify-center
>
<h1 class="display-2 mb-3" style="color:black;">Blog</h1>
<h4 class="subheading" style="color:black;">hhhhhhhafadsjfjasdf</h4>
<h4 class="subheading" style="color:black;">blabla的个人博客站,深挖网站编程艺术</h4>
</v-layout>
</v-parallax>
<!-- the blog list -->
<v-container>
<v-layout row wrap>
<v-flex d-flex xs12 sm6>
<v-card>
<v-toolbar color="primary" dark>
<v-toolbar-title>博客列表</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon fab flat small @click="pageMinus">
<v-icon>keyboard_arrow_left</v-icon>
</v-btn>
<v-btn fab small dark flat>{{page}}</v-btn>
<v-btn icon fab flat small @click="pagePlus">
<v-icon>keyboard_arrow_right</v-icon>
</v-btn>
</v-toolbar>
<v-list three-line :expand="true">
<div v-for="(item, index) in items" :key="item.id">
<v-list-tile
avatar
ripple
@click="toDetail(item.id)"
>
<v-list-tile-content>
<v-list-tile-title><strong>{{ item.title }}</strong></v-list-tile-title>
<v-list-tile-sub-title class="text--primary">{{ item.headline }}</v-list-tile-sub-title>
<v-list-tile-sub-title>{{ item.subtitle }}</v-list-tile-sub-title>
<div>
<v-chip outline color="pink" text-color="red" small v-for="cate in item.categories" :key="cate.id">
{{cate}}
</v-chip>
</div>
</v-list-tile-content>
<v-list-tile-action>
<v-list-tile-action-text>{{ item.action }}</v-list-tile-action-text>
<v-icon
color="yellow darken-2"
>
keyboard_arrow_right
</v-icon>
</v-list-tile-action>
</v-list-tile>
<v-divider
v-if="index + 1 < items.length"
></v-divider>
</div>
</v-list>
</v-card>
</v-flex>
<v-flex d-flex xs12 sm5 offset-sm1>
<v-layout row wrap>
<v-flex d-flex>
<v-layout row wrap>
<h2 style="margin-top:16px;">最新博文:</h2>
<v-flex
d-flex
xs12
v-for="post in posts" :key="post.id"
>
<v-card class="my-3" hover>
<v-img
v-if="post.imgUrl"
class="white--text"
height="150px"
:src="post.imgUrl"
>
<v-container fill-height fluid>
<v-layout>
<v-flex xs12 align-end d-flex>
<span class="caption">{{post.date}}</span>
</v-flex>
</v-layout>
</v-container>
</v-img>
<v-card-title class="headline"><strong>{{ post.title }}</strong></v-card-title>
<v-card-text>
{{ post.subtitle }}
</v-card-text>
<v-card-actions>
<v-chip outline color="pink" text-color="red" small v-for="cate in post.categories.slice(0,3)" :key="cate">
{{cate}}
</v-chip>
<v-spacer></v-spacer>
<v-btn @click="toDetail(post.id)" flat class="blue--text">查看博文</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-flex>
<v-dialog
v-model="openLoader"
hide-overlay
persistent
width="300"
>
<v-card
color="primary"
dark
>
<v-card-text>
请稍候
<v-progress-linear
indeterminate
color="white"
class="mb-0"
></v-progress-linear>
</v-card-text>
</v-card>
</v-dialog>
</v-layout>
</v-flex>
</v-layout>
</v-container>
</div>
</template>
<script>
import axios from 'axios';
export default {
head: {
title: "博客 - 首页",
meta: [
{
hid: 'description',
name: 'description',
content: 'blog description'
},
{name: 'keywords', content: '博客,代码,技术,web开发'},
{name:"baidu-site-verification", content: "nVF2mYh7tG"}
]
},
asyncData() {
return axios.get('https://blabla.blabla.com/blog/index?page=1').then(res => {
return axios.get('https://blabla.blabla.com/blog/index?page='+1).then(res1 => {
return {
items: res.data,
posts: res1.data.splice(0, 4)
};
});
});
},
data () {
return {
openLoader: true,
bgHeight: "920",
title: 'Your Logo',
page: 1,
posts: [
],
items: [
],
isMaxPage: false
}
},
mounted() {
this.openLoader = false;
},
methods: {
toggle (index) {
const i = this.selected.indexOf(index)
if (i > -1) {
this.selected.splice(i, 1)
} else {
this.selected.push(index)
}
},
toDetail(id) {
location.href = "/blog/"+id;
},
pageMinus() {
if (this.page == 1) {
return ;
}
this.page--;
},
pagePlus() {
if (this.isMaxPage) {
return ;
}
this.page++;
}
},
watch: {
page(val) {
this.openLoader = true;
this.$axios.get('https://blabla.blabla.com/blog/index?page='+val).then(res => {
this.items = res.data;
if (this.items.length < 7) {
this.isMaxPage = true;
} else {
this.isMaxPage = false;
}
this.openLoader = false;
});
}
}
}
</script>
<style>
.v-parallax__image {
filter: blur(9px)
}
.v-list--three-line .v-list__tile {
height: 175px;
}
.v-chip--small {
height: 18px;
}
</style>
</code></pre>
<p>对这一部分代码的解读:</p>
<p>由于博客站使用的是vuetify编写的,故而引用了vuetify作为网站的UI插件。</p>
<h4>布局</h4>
<p>写法与单页应用类似,但要注意几个不同点:</p>
<ul><li>单页应用一般会用vue-router的写法表示加载路由页面内容的位置:</li></ul>
<pre><code class="vue"><router-view></router-view></code></pre>
<p>而在nuxt中,要写成</p>
<pre><code class="js"><nuxt/></code></pre>
<ul>
<li>created和data中的逻辑,是在服务端加载时处理的,并不是浏览器端,浏览器端的逻辑比如window或location等对象要在mounted中写,否则会报错。</li>
<li>head中定义一些元数据,这些元数据会被爬虫抓取到,可以在每一个页面中自定义。</li>
</ul>
<h4>页面</h4>
<ul>
<li>单文件组件中的模板的写法与单页应用并无而已,直接写就好,只是记住不要在模板中写js逻辑</li>
<li>vue实例中head中可以定义的变量就是指<code><head></head></code>中定义的参数,例如本例:</li>
</ul>
<pre><code class="js">head: {
title: "首页",
meta: [
{
hid: 'description',
name: 'description',
content: '我就是一个小站点!'
},
{name: 'keywords', content: '博客,代码,技术,开发'},
{name:"google-site-verification", content:"RHlJ7VR51QWbIQFsW_s5qQrbbQPNBkTwhVLCgbFu_6g"},
{name:"baidu-site-verification", content: "nVF2mYh7tG"}
]
}</code></pre>
<p>将会被node渲染为如下html:</p>
<pre><code class="js"><head>
<title>首页</title>
<meta hid='description' name='description' content= '我就是一个小站点' />
<meta name='keywords' content='博客,代码,技术,开发' />
<meta name='google-site-verification' content='RHlJ7VR51QWbIQFsW_s5qQrbbQPNBkTwhVLCgbFu_6g' />
<meta name='baidu-site-verification' content= 'nVF2mYh7tG' />
</head></code></pre>
<p>这也是SEO的一个关键点,请注意。</p>
<ul><li>在渲染页面之前,如果有一些原始数据是需要从外部拿到后才可以继续的,使用<code>asyncData</code>异步获取数据,这里异步获取数据会在数据完全获取完毕后才会去渲染页面,例如本例:</li></ul>
<pre><code class="js">asyncData() {
return axios.get('https://api.fshkehfahsfua.com/blog/list?page=1').then(res => {
return axios.get('https://api.blohfhsldfhl.com/blog/listpo2?page='+1).then(res1 => {
return {
items: res.data,
posts: res1.data.splice(0, 4)
};
});
});
},</code></pre>
<p>这里要注意一下:asyncData中定义的数据,最好在data中也定义一下,因为asyncData的数据会覆盖data。</p>
<pre><code class="js">data() {
return {
posts: [],
items: [],
}
}</code></pre>
<p>哦对了,还有<code>blog</code>详情页<code>_id.vue</code></p>
<blockquote>
<code>_id.vue</code>表示可以用形似<code>blog/123</code>来进行访问,这是vuejs单文件组件的常用写法,这里不赘述。</blockquote>
<pre><code class="js"><template>
<div>
哈哈哈这里是详情页,敏感代码不贴了~
</div>
</template>
<script>
import axios from 'axios';
let initId = 0;
export default {
validate({params}) {
initId = params.id;
return /^\d+$/.test(params.id)
},
head() {
return {
title: this.title,
meta: [
{hid: 'description', name: "description", content: this.descript},
{name: "keywords", content: this.keywords},
],
}
},
asyncData() {
},
data() {
}
}
</script></code></pre>
<blockquote>获取那个传过来的ID,就用validate()中的写法,在下面用的时候,就直接使用initId便可</blockquote>
<h3>结尾</h3>
<p>以上是一些源码的解析,本地运行命令<code>npm run dev</code>或<code>npm run start</code>便可。</p>
<h3>资源链接</h3>
<ul>
<li><a href="https://link.segmentfault.com/?enc=%2F4mozjdwA8hIO%2BhZAmFzsQ%3D%3D.OwQLUNlmHHElJVGsys%2F3UXXMk0x6kNckgJoF53qLD8z2XLP2Dp2VZg4aJIZu%2F9oV" rel="nofollow">vue-ssr</a></li>
<li><a href="https://link.segmentfault.com/?enc=qo2q1B7q4MT5WoqNIKaLnQ%3D%3D.PN6p16Y1%2BQl0pYML%2FSWn0Z47R9eTkAkfecBjlfXGzvevJhdi8zJcXR79aH9XOyM6" rel="nofollow">nuxtjs</a></li>
<li><a href="https://link.segmentfault.com/?enc=WV7%2BNKoKISeWCFsca2lfYw%3D%3D.H2CkSCJgWFH850PDzl9IaF9LKHwFG%2FJXhauiQpli6PY%3D" rel="nofollow">vuetify</a></li>
<li><a href="https://link.segmentfault.com/?enc=otv%2BivzFnxW6mfvxXj73ig%3D%3D.usWqgiIb6InxIVZvUvsS0TxNn6%2FrA6rziZBIWCXJf7AvezIFNt7WKkKR%2B%2B8078OM" rel="nofollow">iview</a></li>
<li><a href="https://link.segmentfault.com/?enc=lBUn4%2BN0VyImPt7zV40eCg%3D%3D.Po4vgCt7nikhkSqepC9oNBfWQ%2BJRYj%2FGMw3IDF6yN64f3Mp8iSVXlmzpaGP4bUK4" rel="nofollow">一个完美的nodejs进程管理工具pm2</a></li>
</ul>
imageAI图像识别,并用python搭建本地服务
https://segmentfault.com/a/1190000019697070
2019-07-08T15:09:36+08:00
2019-07-08T15:09:36+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
1
<p>imageai是一套开源免费的,可以用于图像智能识别的python应用包,使用门槛不高,基本上按照官方文档去写就可以简单实现利用已经训练好的模型识别图像中的物体。<br>imageai图像预检目前实现了四种模型的算法支持,分别是SqueezeNet,ResNet,InceptionV3 和 DenseNet。不同的包对应的训练模型大小不一致,精度也不一致。</p>
<ul>
<li>SqueezeNet(文件大小:4.82 MB,预测时间最短,精准度适中)</li>
<li>ResNet50 by Microsoft Research (文件大小:98 MB,预测时间较快,精准度高)</li>
<li>InceptionV3 by Google Brain team (文件大小:91.6 MB,预测时间慢,精度更高)</li>
<li>DenseNet121 by Facebook AI Research (文件大小:31.6 MB,预测时间较慢,精度最高)</li>
</ul>
<p>imageai不仅可以用于图像识别,还可以进行图形对象检测,视频对象预测和跟踪,自定义训练模型等。对于机器学习不够深入的人员非常友好,可以几行代码实现一个AI应用。</p>
<p>更多深入的内容可以查看<a href="https://link.segmentfault.com/?enc=NgePw7dVerpBscCIw5U19w%3D%3D.e2luy6I%2BaICLIVZr6xwprYWKZCp6jXq8MUCShaiWV1ThXIOXW3jU2Pf7sW5j0JhMEdxdPnp2oWGSjKReq2OgOg%3D%3D" rel="nofollow">原文</a></p>
<p>下面,简单了解一下如何在python本地服务中搭建一个图像识别应用。</p>
<h3>安装imageAI的依赖</h3>
<h4>imageAI的依赖</h4>
<p>python(>=3.5.1)<br>tensorflow(>=1.4.0)<br>Numpy(>=1.13.1)<br>SciPy(>=0.19.1)<br>openCv<br>pillow<br>matplotlib<br>h5py<br>keras</p>
<p>若安装好了pip3 可以直接运行 <code>pip3 install tensorflow numpy scipy opencv-python pillow matplotlib h5py keras imageai</code></p>
<h3>imageAI的训练模型</h3>
<p>imageAI支持使用在<a href="https://link.segmentfault.com/?enc=NZHkF9cP9Qrx5mTSlhFLYA%3D%3D.3dg9LrFdwXTtM1ELUOYv%2BcwSTsE2f8uUi%2BIpasXQHJU%3D" rel="nofollow">ImageNet-1000数据集</a>上训练的4种不同的机器学习算法,也支持在<a href="https://link.segmentfault.com/?enc=00SR33SQON8LFBWQKj%2FeDw%3D%3D.IQIfoO8zd1cXxBGrUKUOog0BxYl0v8ldbGJ3t2o94g0%3D" rel="nofollow">CoCo数据集集</a>上训练的对象检测。</p>
<h3>图像预检</h3>
<p>需要下载一个<a href="https://link.segmentfault.com/?enc=X3lnj8NbBj%2FiLM7NNSoAsQ%3D%3D.GdqwM%2BC9h7%2BYDPjAsu%2F7r3p6GbdUVgUyhzA%2FfyGSQR7K6D%2FJGKQIjis0QKS69gCT88KYSCR6djxJXdRuvwkwBHneaSbWa1tP0%2BiXwqx%2BvkY%3D" rel="nofollow">训练模型</a>。</p>
<p>这里就直接使用facebookAI的训练模型<a href="https://link.segmentfault.com/?enc=mC4%2FS5fsD4UBlyPf79EMXQ%3D%3D.IXe0ZYD99G8oOl6l2bzf8wlNDDq0U8KGTkM3uTzMIar0B5FnkKKhijmJbNJavFsLeoGI%2FRyYY8CRwP4YM8emOzMWn5FRZor7AMvRoPpttcmR9VQoFv1Gd7fzNW7WVKhI" rel="nofollow">DenseNet121</a>。</p>
<p>废话不多说,上代码:</p>
<pre><code class="python">from imageai.Prediction import ImagePrediction
import os,json
execution_path = os.getcwd() # 当前命令行执行路径
prediction = ImagePrediction() # 获取这个图像预测实例
prediction.setModelTypeAsDenseNet() # facebook AI,此处要根据下一步加载的模型文件来确立对应的model类型,其他还有setModelTypeAsSqueezeNet(普通) setModelTypeAsResNet(微软模型) setModelTypeAsInceptionV3(谷歌模型),训练识别精确度依次提升,facebook最准确
prediction.setModelPath(os.path.join(execution_path, "DenseNet-BC-121-32.h5")) # 加载模型文件,就是刚刚下载好的那个
prediction.loadModel(prediction_speed="fast") # 速度调节,如果配置这个则需要使用精确度较高的模型,否则会出现识别率下降的问题
predictions, probabilities =prediction.predictImage(os.path.join(execution_path, "test.jpg"), result_count=6) # 对应的图像识别路径,以及相应的返回结果数量
predictionDict = {}
for eachPrediction, eachProbability in zip(predictions, probabilities):
predictionDict[eachPrediction] = eachProbability
res = json.dumps(predictionDict)
print(res) # 输出结果</code></pre>
<blockquote>虽然这里设置了"fast",但其实对于单核来说运行速度仍然比较慢,如果你的运行服务器是多核的,可以试试并行计算,把多个核都用起来,效率会有不少提升。</blockquote>
<p>代码对应行都有相应注释,看一下打印结果:</p>
<pre><code class="python">{"loudspeaker": 33.06401073932648, "modem": 26.803183555603027, "hard_disc": 7.0777274668216705, "projector": 4.804840311408043, "lighter": 2.9418328776955605, "electric_fan": 1.8662691116333008}</code></pre>
<p>返回的是key-value的json格式,key代表物品名,value代表可能性性百分比数值,最高100,最低0,分别表示准确识别和不可能,由于训练模型使用的是FacebookAI类型的,所以标签名也是英文的,这对于中文用户来说可能需要一个转译的过程。</p>
<blockquote>如何使用python进行中英文翻译,这里不赘述,pip安装googletrans试试看,不过需要翻墙。</blockquote>
<p>好了,基本的识别逻辑有了,我们可以搭建一个http服务,使用get请求来接收一个图片文件名,然后让Python帮我们识别上传的图片里都有什么内容。</p>
<p>上代码:</p>
<pre><code class="python">from http.server import HTTPServer,BaseHTTPRequestHandler
import io,shutil,urllib
from imageai.Prediction import ImagePrediction
import os,json
class MyHttpHandler(BaseHTTPRequestHandler):
def do_GET(self):
name=""
if '?' in self.path:
self.queryString=urllib.parse.unquote(self.path.split('?',1)[1])
#name=str(bytes(params['name'][0],'GBK'),'utf-8')
params=urllib.parse.parse_qs(self.queryString)
name=params["name"][0] if "name" in params else None
r_str=name
enc="UTF-8"
encoded = ''.join(r_str).encode(enc)
if name:
execution_path = os.getcwd()
if execution_path == '' or execution_path == '/':
execution_path = '/www-root/blog' # 因为网关模式执行Python可能导致当前执行路径为'/'或者空的情况。
prediction = ImagePrediction()
prediction.setModelTypeAsDenseNet() # facebook AI
prediction.setModelPath(os.path.join(execution_path, "DenseNet-BC-121-32.h5"))
prediction.loadModel(prediction_speed="fast")
predictions, probabilities = prediction.predictImage(os.path.join(execution_path, "image/"+name), result_count=3)
predictionDict = {}
for eachPrediction, eachProbability in zip(predictions, probabilities):
predictionDict[eachPrediction] = eachProbability
res = json.dumps(predictionDict)
encoded = ''.join(res).encode(enc)
f = io.BytesIO()
f.write(encoded)
f.seek(0)
self.send_response(200)
self.send_header("Content-type", "text/html; charset=%s" % enc)
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
shutil.copyfileobj(f,self.wfile)
httpd=HTTPServer(('',8080),MyHttpHandler)
print("Server started on 127.0.0.1,port 8080.....")
httpd.serve_forever()</code></pre>
<p>在服务器运行<code>python3 你的脚本名.py</code>就可以完美运行啦!</p>
<pre><code class="shell">curl -X GET 'http://127.0.0.1:8080?name=IMG_8590.JPG'
# response
{"loudspeaker": 33.06401073932648, "modem": 26.803183555603027, "hard_disc": 7.0777274668216705, "projector": 4.804840311408043, "lighter": 2.9418328776955605, "electric_fan": 1.8662691116333008}</code></pre>
<h3>结尾</h3>
<p>本文只介绍了图像识别以及如何加在python http server中方便内部调用,其实还可以做得更多,比如图形对象检测,视频对象跟踪等,你也可以自定义训练模型,按照对应的4种算法处理模型数据,搞一个自定义AI产品。</p>
php中使用protobuffer
https://segmentfault.com/a/1190000019513723
2019-06-18T16:09:27+08:00
2019-06-18T16:09:27+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
2
<h3>Protobuf 简介</h3>
<p>protobuf(Protocol buffers)是谷歌出品的跨平台、跨语言、可扩展的数据传输及存储的协议,是高效的数据压缩编码方式之一。</p>
<p>Protocol buffers 在序列化数据方面,它是灵活的,高效的。相比于 XML 来说,Protocol buffers 更加小巧,更加快速,更加简单。一旦定义了要处理的数据的数据结构之后,就可以利用 Protocol buffers 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。</p>
<p>Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。</p>
<p>此外,Protobuf由于其在内网高效的数据交换效率,是被广泛应用于微服务的,在谷歌的开源框架grpc即是基于此构建起来的。</p>
<h4>php-protobuf安装</h4>
<p>由于protobuf原生并不支持php,所以php如果使用pb则需要安装相应扩展。</p>
<pre><code class="shell">pecl install protobuf</code></pre>
<p>环境中需要有<code>protoc</code>编译器,下载安装方式:</p>
<pre><code class="shell">$ wget https://github.com/google/protobuf/releases/download/v2.5.0/protobuf-2.5.0.tar.gz
$ tar zxvf protobuf-2.5.0.tar.gz
$ cd protobuf-2.5.0
$ ./configure --prefix=/usr/local/protobuf
$ sudo make
$ sudo make install</code></pre>
<p>验证安装成功:</p>
<pre><code class="shell">$ /usr/local/protobuf/bin/protoc --version
libprotoc 2.5.0</code></pre>
<p>php-protobuf安装成功</p>
<pre><code class="shell">php --ri protobuf</code></pre>
<h3>安装<a href="https://link.segmentfault.com/?enc=C1MyA7ssrUesXja0XLlAlg%3D%3D.JbEosQ3X9gniFBmL8w%2B3GMp9e2dEAqAxlSq8eJKthMU%3D" rel="nofollow">lumen</a>和<a href="https://link.segmentfault.com/?enc=KMtSJWb0u0hhw4jNRgNJSg%3D%3D.x5VArUFkFbSwQrwhTuHt34mAGqbfJkaN1kb4RHOw0upzcsuOdtdNml8w372%2FTFHx" rel="nofollow">google/protobuf</a>依赖</h3>
<pre><code class="shell">lumen new rpc</code></pre>
<blockquote>
<code>lumen new rpc</code>命令相当于<code>composer create-project laravel/lumen rpc</code>
</blockquote>
<pre><code class="shell">composer require google/protobuf</code></pre>
<p>在<code>composer.json</code>下添加classmap:</p>
<pre><code class="json">{
"classmap": [
"protobuf/"
]
}</code></pre>
<p>ok,准备工作都已做好了。</p>
<h3>自己做一个demo</h3>
<p>在代码目录下创建一个protobuf文件夹<code>mkdir protobuf</code></p>
<p>进入该目录,创建一个文件<code>searchRequest.proto</code></p>
<pre><code class="proto">syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}</code></pre>
<h4>?此处很重要?</h4>
<blockquote>在<code>composer.json</code>下添加classmap,否则将无法侦测到对应class</blockquote>
<pre><code class="json">{
"classmap": [
"protobuf/"
]
}</code></pre>
<p>在命令行下运行:<code>protoc --proto_path=protobuf/ --php_out=protobuf/ protobuf/searchRequest.proto && composer dump-autoload</code></p>
<p>现在你看到的代码目录,应该是这样的:</p>
<p><img src="/img/bVbt2pY?w=406&h=398" alt="clipboard.png" title="clipboard.png"></p>
<p>现在,我们需要的请求类已经得到了,现在看看如何使用它!</p>
<h3>使用</h3>
<p>在web.php创建一个路由</p>
<pre><code class="php">$router->get('testp', 'ExampleController@testProtobuf');</code></pre>
<p>在<code>ExampleController</code>下添加:</p>
<pre><code class="php"> public function testProtobuf()
{
// require_once base_path('protobuf/SearchRequest.php');
$request = new \SearchRequest();
$request->setPageNumber(67);
dd($request->getPageNumber());
}</code></pre>
<p>如果正常打印出<code>67</code>这个数字,就表示该类可以用,恭喜你已经成功完成了一个请求类的创建。</p>
<h4>Go deeper</h4>
<p>现在,看一下生成的SearchRequest都有哪些方法:</p>
<pre><code class="php">array:16 [▼
0 => "__construct"
1 => "getQuery"
2 => "setQuery"
3 => "getPageNumber"
4 => "setPageNumber"
5 => "getResultPerPage"
6 => "setResultPerPage"
7 => "getCorpus"
8 => "setCorpus"
9 => "clear"
10 => "discardUnknownFields"
11 => "serializeToString"
12 => "mergeFromString"
13 => "serializeToJsonString"
14 => "mergeFromJsonString"
15 => "mergeFrom"
]</code></pre>
<p>这里面带有set前缀的方法,都是设定对应字段的,带有get的,都是从buffer中获取值的,里面的SerializeToString,建议阅读官方文档,里面有对应的合理的解释。</p>
<h4>和grpc的结合</h4>
<p><code>composer install grpc/grpc</code></p>
<p>定义Service,这一个需要在client和server两端都要完成</p>
<pre><code class="shell">service RouteGuide {
rpc GetFeature(Point) returns (Feature) {}
rpc RecordRoute(stream Point) returns (RouteSummary) {}
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}</code></pre>
<p><code>protoc</code>生成对应的Service实例。</p>
<p>创建一个client</p>
<pre><code class="php">$client = new Routeguide\RouteGuideClient('localhost:50051', [
'credentials' => Grpc\ChannelCredentials::createInsecure(),
]);</code></pre>
<p>调用RPC服务</p>
<pre><code class="php">$point = new Routeguide\Point();
$point->setLatitude(409146138);
$point->setLongitude(-746188906);
list($feature, $status) = $client->GetFeature($point)->wait();</code></pre>
<p>grpc更多实现,请参阅<a href="https://link.segmentfault.com/?enc=DUct2EI9KmweeEu0vatK%2BA%3D%3D.%2Fs8bAg5BhQ0kIu2Ifzu7XVcRXp5Vj9s6dSZ8I1rkYO3ClsCegT2DV3YrJ%2BQOZwYN" rel="nofollow">官方文档</a>、<a href="https://link.segmentfault.com/?enc=kx%2FyQfAOcG1gdGd8Xzp5Gg%3D%3D.YGAIn5XHMLd0SPJ5%2BTMGovnhsbsHeb8MecL%2F2nOHY%2BIt%2FNBch9X8pvrR%2FVE7xeqZ" rel="nofollow">快速指南</a></p>
php业务代码风格的几个建议
https://segmentfault.com/a/1190000018840335
2019-04-12T01:08:41+08:00
2019-04-12T01:08:41+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
2
<h2>业务代码风格的几个建议</h2>
<ol>
<li>
<h4>关于循环体内重复调用的问题</h4>
<p>代码示例:</p>
<pre><code class="php">// $array是一个可能超出1000个数据的大变量
foreach ($array as $ar) {
$model = new Model();
$modelData = $model->findOne(['id' => $ar['id']]);
$modelData['is_less'] = $modelData['tag'] == 3 ? true : false;
$model2 = new AnotherModel();
$model2->find(['id' => $ar['id']])->update(['is_less' => $modelData['is_less']]);
}</code></pre>
<p>以上代码块是有非常严重问题的。</p>
<p>在循环体中,不能重复使用数据库查询太多次,尤其是相似或一致的sql,一定要批量查询获取数据之后再做相应逻辑层面的处理。如果循环次数较多,不仅仅会体现在循环逻辑较慢上,而且在并发读写的业务中由于频繁读取硬盘以及锁表等可能会给数据库服务器造成巨大压力。</p>
<p>所以以上代码可以改造成:</p>
<pre><code class="php">$ids = array_columns($array, 'id');
if ($ids) {
$model = new Model();
$modelDataList = $model->where(['in', 'id', $ids])->all();
$modelDataList = array_combine(array_columns($modelDataList, 'id'), $modelDataList);
}
foreach ($array as $ar) {
$modelData = empty($modelDataList[$ar['id']]) ? [] : $modelDataList[$ar['id']];
if (!$modelData) {
continue;
}
$modelData['is_less'] = $modelData['tag'] == 3 ? true : false;
$model2 = new AnotherModel();
$model2->find(['id' => $ar['id']])->update(['is_less' => $modelData['is_less']]);
}</code></pre>
<p>还有一个例子,如:</p>
<pre><code class="php">foreach($ids as $id) {
$data = RpcService::getMyData(['my_id' => $id]);
$data['field1'] = $data['field2'] + $data['field3'];
$sendPost = RpcService::sendToBoss(['field1' => $data['field1']]);
}</code></pre>
<p>像这种通过接口获取数据或者更新数据的,一般不能在循环体内重复调用,因为http或者其他实现rpc的网络协议或多或少都会慢在数据传输上,而且对方业务的实现一般也不建议调用方循环调用,所以如果有批量调用接口的需求,应该要求接口提供方提供批量操作的接口,在循环体外进行操作。</p>
<p>以上代码可修改为:</p>
<pre><code class="php">$dataList = RpcService::getMyDataList(['ids' => $ids]);
array_walk($dataList, function () {
// 处理字段
});
RpcService::multiSendToBoss(['list' => array_columns($dataList, 'field1')]);</code></pre>
<p>还有文件读取也是类似,php读写文件效率并不高,应避免频繁读写相同文件。例如:</p>
<pre><code class="php">$readFilePath = 'current_file';
foreach($writeFilePaths as $path) {
$content = file_get_content(realpath($readFilePath));
file_put_content($path, $content);
}</code></pre>
<p>应将文件内容放在循环体外部。</p>
<p>其他但凡是有耗时或不建议频繁调用的逻辑,都应该写在循环体外。</p>
</li>
<li>
<h4>关于业务层面类调用或方法调用可读性的问题。</h4>
<p>关于这一点,先看一下代码示例:</p>
<pre><code class="php">class DemoClass
{
public $handler = [];
public function handlerRegister(Handler $handler)
{
$this->handler[] = $hanlder;
}
public function run($id, $params)
{
foreach ($this->handler as $h) {
if ($h->id === $id) {
return $h->handle($params);
}
}
}
}
class Handler
{
public $id;
public function handle($params)
{
$result = call_user_func($params['callback'], $params['params']);
$resultData = (new $result)->getData();
$next = $params['next'];
return $next($resultData);
}
}</code></pre>
<p>call_user_func、call_user_func_array、Reflection类等可以对变量里的某些内容直接实例化或者调用,这在机器编译运行看来是没什么问题的,但是人看的话就比较费心了,你要追根溯源搞清楚调用的是啥,反射的是啥,虽然写起来很简单粗暴,但对读的人不太友好。况且,即便是phpstorm这样的强大的IDE,也不能帮你识别追溯这些变量的源头。</p>
<p>所以,业务代码应尽量避免使用类似代码。</p>
<p>但是,如果你写的是底层脚手架,是丰富框架功能的一个工具包,那么你可以按照自己的想法来,使用者看不看得懂就不重要了。所以像以上的例子往往出现在vendor依赖包中的比较多,写起来比较简单,也不必担心别人看不懂的问题。</p>
</li>
<li>
<h4>关于辅助方法编写的问题</h4>
<p>这一点不同的框架有不同的表现,可能有一些框架有自身实现思想的考虑,不方便全局调用一些东西,但目前很多框架都使用了容器,所以使用辅助方法进一步精简代码就有些必要了。</p>
<p>写有辅助方法文件,需要在框架加载过程中,业务使用之前引入,最好是全局引入。</p>
<p>比如Yii2中如果要实现一个json返回,需要写以下代码:</p>
<pre><code class="php">Yii::$app->response->format = Response::FORMAT_JSON;
return [
'code' => 1,
'message' => 'success'
];</code></pre>
<p>如果你写了一个辅助方法如下:</p>
<pre><code class="php">function ajax(array $data)
{
Yii::$app->response->format = Response::FORMAT_JSON;
return $data;
}</code></pre>
<p>那么代码可以写成:</p>
<pre><code class="php">return ajax([
'code' => 1,
'message' => 'success'
]);</code></pre>
<p>又比如:获取一个post提交的所有参数:</p>
<pre><code class="php">$post = Yii::$app->request->post(); // 获取所有
$field1 = Yii::$app->request->post('field1'); // 获取其中的某个参数</code></pre>
<p>辅助方法如下:</p>
<pre><code class="php">function post($key = null, default = null)
{
if ($key === null) {
return Yii::$app->request->post(); // 获取所有
}
return Yii::$app->request->post($key, $default)
}</code></pre>
<p>调用如下:</p>
<pre><code class="php">$post = post();
$field1 = post('field1');</code></pre>
<p>又比如,根据键销毁数组中的某一个值。</p>
<pre><code class="php">// 原生写法
if (isset($arr[$key])) {
unset($key);
}
// 辅助方法
function array_pull(&$arr, $key)
{
if (isset($arr[$key])) {
unset($key);
}
}
// 调用
array_pull($arr, $key);</code></pre>
</li>
<li>
<h4>关于逻辑块复用和可读性的问题</h4>
<p>比如:</p>
<pre><code class="php">public function handle($params)
{
$dataList = (new Model)->query($params)->all();
$return = [];
foreach ($dataList as $data) {
if ($data['key'] == 1) {
$data['field1'] = "123";
} else if ($data['key'] == 2) {
$data['field1'] = '678';
$data['field2'] = '7hj';
} else if ($data['key'] == 3) {
$data['field1'] = 'uyo';
} else {
$data['field1'] = 'other';
}
if ($data['field1'] == "123") {
$other = [
'other1' => 34,
'other2' => 35,
'other3' => 98
];
$return[] = $other;
} elseif ($data['field1'] == "678") {
$other = [
'other1' => 341,
'other2' => 351,
'other3' => 981
];
if ($data['field2'] == '7hj') {
$other = [
'other1' => 3412,
'other2' => 3512,
'other3' => 9812
];
}
$return[] = $other;
} else if ($data['field1'] === 'uyo') {
$other = [
'other1' => 3412,
'other2' => 3512,
'other3' => 9812
];
$return[] = $other;
} else {
$other = [
'other1' => 34123,
'other2' => 35123,
'other3' => 98123
];
$return[] = $other;
}
}
}</code></pre>
<p>以上代码出现的问题主要有三个:无注释、判断条件太多、逻辑块无法复用。</p>
<p>如果出现更复杂的逻辑,这段代码无疑可能会超过100多行,这在开发维护人员看起来是很艰难的。</p>
<p>注释问题和判断条件太多可能由于业务的问题,有时候无法避免,这里重点说一下逻辑块复用。</p>
<p>以上的代码片段中,<code>dataList</code>的获取,应该是一个独立的方法,因为将来可能其他功能也会使用同样的方法获取对应数据;针对于<code>data['field1']</code>的取值,也应该是一个独立的方法;下面对于<code>data['field1']</code>的判断,也应该是一个独立方法。这就需要对代码拆分,既能够保证代码简洁性、复用性和可读性,也避免多个无用变量在一个逻辑块中积累。</p>
<p>在代码逻辑中,一个功能应该由一个方法来实现,一个方法也应该只做一件事。</p>
<p>这样把这段代码拆分之后,将会变为:</p>
<pre><code class="php">public function getDataList($params)
{
return (new Model)->query($params)->all();
}
public function handleField($data)
{
if ($data['key'] == 1) {
$data['field1'] = "123";
} else if ($data['key'] == 2) {
$data['field1'] = '678';
$data['field2'] = '7hj';
} else if ($data['key'] == 3) {
$data['field1'] = 'uyo';
} else {
$data['field1'] = 'other';
}
return $data;
}
public function getOther()
{
if ($data['field1'] == "123") {
$other = [
'other1' => 34,
'other2' => 35,
'other3' => 98
];
} elseif ($data['field1'] == "678") {
$other = [
'other1' => 341,
'other2' => 351,
'other3' => 981
];
if ($data['field2'] == '7hj') {
$other = [
'other1' => 3412,
'other2' => 3512,
'other3' => 9812
];
}
} else if ($data['field1'] === 'uyo') {
$other = [
'other1' => 3412,
'other2' => 3512,
'other3' => 9812
];
} else {
$other = [
'other1' => 34123,
'other2' => 35123,
'other3' => 98123
];
}
return $other;
}
public function handle($params)
{
$dataList = $this->getDataList($params);
$return = [];
foreach ($dataList as $data) {
$data = $this->handleField($data);
$return[] = $other;
}
return $return;
}</code></pre>
<p>这几个方法各自承担一个功能,只完成一件事。<code>handle()</code>方法只是用来组织几个方法的数据,简单明了。</p>
<p>建议每个方法的代码不超过30行,除非情况特殊。</p>
</li>
<li>
<h4>关于公共方法书写风格的建议</h4>
<p>关于公共方法调用的代码风格,应该遵循调用者最少知道的原则,调用者只需按照对应参数传入即可,后面的逻辑复杂性,原则上不必被调用者知道,且调用者应能够充分知晓正确与错误信息,且应保证其健壮性。</p>
<p>涉及到传入参数为数组的,应告知调用者该数组内部参数的详细说明,或在注释中给出对应示例。</p>
</li>
<li>
<h4>页面下载使用专用文件服务器</h4>
<p>涉及到web页面直接生成数据下载的,应尽量使用专用的文件服务器,而不是直接在页面进行下载,且下载应尽量使用异步生成文件。</p>
<p>例如用户在点击页面下载按钮之后跳入自己的下载页面,这个页面上有自己的文件下载的历史表格,有状态标记文件是否可以进行下载,当后台将文件生成好上传到文件服务器之后,会标记成可下载。</p>
<p>好处:记录下载历史、历史文件下载、下载性能优化、可以处理大文件。</p>
<p>坏处:需要多做一个页面和一张表,且需要等待文件生成上传至文件服务器的时间。</p>
<p>文件服务器可以使用对象云。</p>
</li>
</ol>
centos使用chrome-cli、chromium或wkhtmltoimage截图时出现的中文字符乱码的解决方案
https://segmentfault.com/a/1190000018687409
2019-03-28T14:10:23+08:00
2019-03-28T14:10:23+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
1
<h2>在<code>centos7</code>环境下使用<code>chrome-php</code>或<code>wkhtmltoimage</code>截图时出现的中文乱码解决方案</h2>
<blockquote>最近做了一个小项目,要求使用<code>chrome/chromium</code>对抓取的页面进行截图保存并上传云服务,因为是<code>composer</code>依赖包管理,所以使用了<a href="https://link.segmentfault.com/?enc=yslKAZ6GUnrZ4bYl1PRjEA%3D%3D.1%2FWcFwttD1Oi0PTajwa89ZuLwIzL%2FmC81AJA8bLIUAHVa61bi6yD6eM4yg0VcHPwhZzf8yR7K4CdeH6xRp2cMw%3D%3D" rel="nofollow">chrome-php</a>
</blockquote>
<p>核心代码示例:</p>
<pre><code class="php"> // navigate
$navigation = $page->navigate('https://www.baidu.com');
// wait for the page to be loaded
$navigation->waitForNavigation();
// take a screenshot
$screenshot = $page->screenshot([
'format' => 'jpeg', // default to 'png' - possible values: 'png', 'jpeg',
'quality' => 80 // only if format is 'jpeg' - default 100
]);
// save the screenshot
$screenshot->saveToFile('/some/place/file.jpg');</code></pre>
<p>结果发现截图不正确,所有中文字符乱码:</p>
<p><img src="/img/remote/1460000018687412?w=800&h=600" alt="" title=""></p>
<p>后来提了issue,<a href="https://link.segmentfault.com/?enc=4QHHNSt49nOYCRbmbk87mQ%3D%3D.biXnJJU0dGOxJxceBmosw8DBPShy10n1sRfkTbWNcCTmVlvsp8K%2FY8kbVTg4OTrNoNDxZY0%2BKHeGQTT%2BQLlFPA%3D%3D" rel="nofollow">地址</a></p>
<p>按照对方给的解决方法,并未有效解决。</p>
<p>后来换了各种系统环境,包括更改中文支持,依然如故,只有在自己的mac上是正常的。</p>
<p>所以猜想应该是字体的问题,所以尝试以下方案,最终正常显示:</p>
<blockquote>如以下命令执行出现<code>permission denied</code>的情况,使用sudo命令执行。</blockquote>
<h4>1.fc-list查看是否有中文字体,一般情况下是不存在的,否则也不会乱码。</h4>
<h4>2.查看是否支持ttmkfdir <code>which ttmkfdir</code>,如果没有的话,那么安装:<code>yum install -y ttmkfdir</code>
</h4>
<h4>3.centos7系统的话,创建字体目录,<code>mkdir /usr/share/fonts/chinese</code>
</h4>
<h4>4.上传本地的字体文件,例如mac里对应的任何一个ttf字体文件。</h4>
<h4>5.将字体文件复制到<code>/usr/share/fonts/chinese</code>下,并执行<code>chmod -R 755 /usr/share/fonts/chinese</code>
</h4>
<h4>6.执行<code>ttmkfdir -e /usr/share/X11/fonts/encodings/encodings.dir</code>
</h4>
<h4>7.编辑<code>/etc/fonts/fonts.conf</code>,在如下部位添加:</h4>
<p><img src="/img/bVbqzAP?w=500&h=175" alt="图片描述" title="图片描述"></p>
<h4>8.运行<code>fc-cache</code>和<code>fc-cache-64</code>(如果有的话)</h4>
<h4>9.运行<code>fc-list</code>查看刚刚安装的字体是否存在。</h4>
<h4>10.再一次运行程序脚本,查看截图是否包含正常的中文字符。</h4>
<p><img src="/img/remote/1460000018687413" alt="" title=""></p>
规则引擎RulerZ用法及实现原理解读
https://segmentfault.com/a/1190000018374191
2019-03-04T01:14:51+08:00
2019-03-04T01:14:51+08:00
Sunday
https://segmentfault.com/u/sunday_5b4d96940bbc6
1
<h2>规则引擎RulerZ用法及实现原理解读</h2>
<p>废话不多说,rulerz的官方地址是:<a href="https://link.segmentfault.com/?enc=LxAXjMpix9kwGYlicnWg%2Fw%3D%3D.vsd8JoMw5GnP4StHZYKt84zwX21dDMbQ92KV%2FCGVy24jITco62iSzs9%2FaRFMvXav" rel="nofollow">https://github.com/K-Phoen/ru...</a></p>
<blockquote>注意,本例中只拿普通数组做例子进行分析</blockquote>
<h4>1. 简介</h4>
<p>RulerZ是一个用php实现的composer依赖包,目的是实现一个数据过滤规则引擎。RulerZ不仅支持数组过滤,也支持一些市面上常见的ORM,如Eloquent、Doctrine等,也支持Solr搜索引擎。<br>这是一个缺少中文官方文档的开源包,当然由于star数比较少,可能作者也觉得没必要。</p>
<h4>2.安装</h4>
<p>在你的项目composer.json所在目录下运行:</p>
<pre><code class="php">composer require 'kphoen/rulerz'</code></pre>
<h4>3.使用 - 过滤</h4>
<p>现有数组如下:</p>
<pre><code class="php">$players = [
['pseudo' => 'Joe', 'fullname' => 'Joe la frite', 'gender' => 'M', 'points' => 2500],
['pseudo' => 'Moe', 'fullname' => 'Moe, from the bar!', 'gender' => 'M', 'points' => 1230],
['pseudo' => 'Alice', 'fullname' => 'Alice, from... you know.', 'gender' => 'F', 'points' => 9001],
];
</code></pre>
<p>初始化引擎:</p>
<pre><code class="php">use RulerZ\Compiler\Compiler;
use RulerZ\Target;
use RulerZ\RulerZ;
// compiler
$compiler = Compiler::create();
// RulerZ engine
$rulerz = new RulerZ(
$compiler, [
new Target\Native\Native([ // 请注意,这里是添加目标编译器,处理数组类型的数据源时对应的是Native
'length' => 'strlen'
]),
]
);</code></pre>
<p>创建一条规则:</p>
<pre><code class="php">$rule = "gender = :gender and points > :min_points'</code></pre>
<p>将参数和规则交给引擎分析。</p>
<pre><code class="php">$parameters = [
'min_points' => 30,
'gender' => 'F',
];
$result = iterator_to_array(
$rulerz->filter($players, $rule, $parameters) // the parameters can be omitted if empty
);
// result 是一个过滤后的数组
array:1 [▼
0 => array:4 [▼
"pseudo" => "Alice"
"fullname" => "Alice, from... you know."
"gender" => "F"
"points" => 9001
]
]</code></pre>
<h4>4.使用 - 判断是否满足规则</h4>
<pre><code>$rulerz->satisfies($player, $rule, $parameters);
// 返回布尔值,true表示满足</code></pre>
<h4>5.底层代码解读</h4>
<p>下面,让我们看看从创建编译器开始,到最后出结果的过程中发生了什么。<br>1.<code>Compiler::create();</code><br>这一步是实例化一个FileEvaluator类,这个类默认会将本地的系统临时目录当做下一步临时类文件读写所在目录,文件类里包含一个has()方法和一个write()方法。文件类如下:</p>
<pre><code class="php">
<?php
declare(strict_types=1);
namespace RulerZ\Compiler;
class NativeFilesystem implements Filesystem
{
public function has(string $filePath): bool
{
return file_exists($filePath);
}
public function write(string $filePath, string $content): void
{
file_put_contents($filePath, $content, LOCK_EX);
}
}
</code></pre>
<p>2.初始化RulerZ引擎,<code>new RulerZ()</code><br>先看一下RulerZ的构建方法:</p>
<pre><code class="php"> public function __construct(Compiler $compiler, array $compilationTargets = [])
{
$this->compiler = $compiler;
foreach ($compilationTargets as $targetCompiler) {
$this->registerCompilationTarget($targetCompiler);
}
}</code></pre>
<p>这里的第一个参数,就是刚刚的编译器类,第二个是目标编译器类(实际处理数据源的),因为我们选择的是数组,所以这里的目标编译器是<code>Native</code>,引擎会将这个目标编译类放到自己的属性<code>$compilationTargets</code>。</p>
<pre><code class="php">
public function registerCompilationTarget(CompilationTarget $compilationTarget): void
{
$this->compilationTargets[] = $compilationTarget;
}</code></pre>
<p>3.运用filter或satisfies方法</p>
<p>这一点便是核心了。<br>以filter为例:</p>
<pre><code class="php"> public function filter($target, string $rule, array $parameters = [], array $executionContext = [])
{
$targetCompiler = $this->findTargetCompiler($target, CompilationTarget::MODE_FILTER);
$compilationContext = $targetCompiler->createCompilationContext($target);
$executor = $this->compiler->compile($rule, $targetCompiler, $compilationContext);
return $executor->filter($target, $parameters, $targetCompiler->getOperators()->getOperators(), new ExecutionContext($executionContext));
}</code></pre>
<p>第一步会检查目标编译器是否支持筛选模式。<br>第二步创建编译上下文,这个一般统一是Context类实例</p>
<pre><code class="php"> public function createCompilationContext($target): Context
{
return new Context();
}</code></pre>
<p>第三步,执行compiler的compile()方法</p>
<pre><code class="php">
public function compile(string $rule, CompilationTarget $target, Context $context): Executor
{
$context['rule_identifier'] = $this->getRuleIdentifier($target, $context, $rule);
$context['executor_classname'] = 'Executor_'.$context['rule_identifier'];
$context['executor_fqcn'] = '\RulerZ\Compiled\Executor\\Executor_'.$context['rule_identifier'];
if (!class_exists($context['executor_fqcn'], false)) {
$compiler = function () use ($rule, $target, $context) {
return $this->compileToSource($rule, $target, $context);
};
$this->evaluator->evaluate($context['rule_identifier'], $compiler);
}
return new $context['executor_fqcn']();
}
protected function getRuleIdentifier(CompilationTarget $compilationTarget, Context $context, string $rule): string
{
return hash('crc32b', get_class($compilationTarget).$rule.$compilationTarget->getRuleIdentifierHint($rule, $context));
}
protected function compileToSource(string $rule, CompilationTarget $compilationTarget, Context $context): string
{
$ast = $this->parser->parse($rule);
$executorModel = $compilationTarget->compile($ast, $context);
$flattenedTraits = implode(PHP_EOL, array_map(function ($trait) {
return "\t".'use \\'.ltrim($trait, '\\').';';
}, $executorModel->getTraits()));
$extraCode = '';
foreach ($executorModel->getCompiledData() as $key => $value) {
$extraCode .= sprintf('private $%s = %s;'.PHP_EOL, $key, var_export($value, true));
}
$commentedRule = str_replace(PHP_EOL, PHP_EOL.' // ', $rule);
return <<<EXECUTOR
namespace RulerZ\Compiled\Executor;
use RulerZ\Executor\Executor;
class {$context['executor_classname']} implements Executor
{
$flattenedTraits
$extraCode
// $commentedRule
protected function execute(\$target, array \$operators, array \$parameters)
{
return {$executorModel->getCompiledRule()};
}
}
EXECUTOR;
}</code></pre>
<p>这段代码会依照crc13算法生成一个哈希串和Executor拼接作为执行器临时类的名称,并将执行器相关代码写进上文提到的临时目录中去。生成的代码如下:</p>
<pre><code class="php">// /private/var/folders/w_/sh4r42wn4_b650l3pc__fh7h0000gp/T/rulerz_executor_ff2800e8
<?php
namespace RulerZ\Compiled\Executor;
use RulerZ\Executor\Executor;
class Executor_ff2800e8 implements Executor
{
use \RulerZ\Executor\ArrayTarget\FilterTrait;
use \RulerZ\Executor\ArrayTarget\SatisfiesTrait;
use \RulerZ\Executor\ArrayTarget\ArgumentUnwrappingTrait;
// gender = :gender and points > :min_points and points > :min_points
protected function execute($target, array $operators, array $parameters)
{
return ($this->unwrapArgument($target["gender"]) == $parameters["gender"] && ($this->unwrapArgument($target["points"]) > $parameters["min_points"] && $this->unwrapArgument($target["points"]) > $parameters["min_points"]));
}
}</code></pre>
<p>这个临时类文件就是最后要执行过滤动作的类。<br>FilterTrait中的filter方法是首先被执行的,里面会根据execute返回的布尔值来判断,是否通过迭代器返回符合条件的行。<br>execute方法就是根据具体的参数和操作符挨个判断每行中对应的cell是否符合判断来返回true/false。</p>
<pre><code class="php"> public function filter($target, array $parameters, array $operators, ExecutionContext $context)
{
return IteratorTools::fromGenerator(function () use ($target, $parameters, $operators) {
foreach ($target as $row) {
$targetRow = is_array($row) ? $row : new ObjectContext($row);
if ($this->execute($targetRow, $operators, $parameters)) {
yield $row;
}
}
});
}</code></pre>
<p>satisfies和filter基本逻辑类似,只是最后satisfies是执行单条判断。</p>
<p>有一个问题,我们的编译器是如何知道我们设立的操作规则<code>$rule</code>的具体含义的,如何parse的?<br>这就涉及另一个问题了,抽象语法树(AST)。</p>
<h4>Go further - 抽象语法树</h4>
<p>我们都知道php zend引擎在解读代码的过程中有一个过程是语法和词法分析,这个过程叫做parser,中间会将代码转化为抽象语法树,这是引擎能够读懂代码的关键步骤。</p>
<p>同样,我们在写一条规则字符串的时候,代码如何能够明白我们写的是什么呢?那就是抽象语法树。</p>
<p>以上面的规则为例:</p>
<p>gender = :gender and points > :min_points</p>
<p>这里, =、and、>都是操作符,但是机器并不知道他们是操作符,也不知道其他字段是什么含义。</p>
<p>于是rulerz使用自己的语法模板。</p>
<p>首先是默认定义了几个操作符。</p>
<pre><code><?php
declare(strict_types=1);
namespace RulerZ\Target\Native;
use RulerZ\Target\Operators\Definitions;
class NativeOperators
{
public static function create(Definitions $customOperators): Definitions
{
$defaultInlineOperators = [
'and' => function ($a, $b) {
return sprintf('(%s && %s)', $a, $b);
},
'or' => function ($a, $b) {
return sprintf('(%s || %s)', $a, $b);
},
'not' => function ($a) {
return sprintf('!(%s)', $a);
},
'=' => function ($a, $b) {
return sprintf('%s == %s', $a, $b);
},
'is' => function ($a, $b) {
return sprintf('%s === %s', $a, $b);
},
'!=' => function ($a, $b) {
return sprintf('%s != %s', $a, $b);
},
'>' => function ($a, $b) {
return sprintf('%s > %s', $a, $b);
},
'>=' => function ($a, $b) {
return sprintf('%s >= %s', $a, $b);
},
'<' => function ($a, $b) {
return sprintf('%s < %s', $a, $b);
},
'<=' => function ($a, $b) {
return sprintf('%s <= %s', $a, $b);
},
'in' => function ($a, $b) {
return sprintf('in_array(%s, %s)', $a, $b);
},
];
$defaultOperators = [
'sum' => function () {
return array_sum(func_get_args());
},
];
$definitions = new Definitions($defaultOperators, $defaultInlineOperators);
return $definitions->mergeWith($customOperators);
}
}
</code></pre>
<p>在RulerZParserParser中,有如下方法:</p>
<pre><code>public function parse($rule)
{
if ($this->parser === null) {
$this->parser = Compiler\Llk::load(
new File\Read(__DIR__.'/../Grammar.pp')
);
}
$this->nextParameterIndex = 0;
return $this->visit($this->parser->parse($rule));
}
</code></pre>
<p>这里要解读一个核心语法文件Grammar.pp,Pascal语法脚本:</p>
<pre><code>//
// Hoa
//
//
// @license
//
// New BSD License
//
// Copyright © 2007-2015, Ivan Enderlin. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the Hoa nor the names of its contributors may be
// used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Inspired from \Hoa\Ruler\Grammar.
//
// @author Stéphane Py <stephane.py@hoa-project.net>
// @author Ivan Enderlin <ivan.enderlin@hoa-project.net>
// @author Kévin Gomez <contact@kevingomez.fr>
// @copyright Copyright © 2007-2015 Stéphane Py, Ivan Enderlin, Kévin Gomez.
// @license New BSD License
%skip space \s
// Scalars.
%token true (?i)true
%token false (?i)false
%token null (?i)null
// Logical operators
%token not (?i)not\b
%token and (?i)and\b
%token or (?i)or\b
%token xor (?i)xor\b
// Value
%token string ("|')(.*?)(?<!\\)\1
%token float -?\d+\.\d+
%token integer -?\d+
%token parenthesis_ \(
%token _parenthesis \)
%token bracket_ \[
%token _bracket \]
%token comma ,
%token dot \.
%token positional_parameter \?
%token named_parameter :[a-z-A-Z0-9_]+
%token identifier [^\s\(\)\[\],\.]+
#expression:
logical_operation()
logical_operation:
operation()
( ( ::and:: #and | ::or:: #or | ::xor:: #xor ) logical_operation() )?
operation:
operand() ( <identifier> logical_operation() #operation )?
operand:
::parenthesis_:: logical_operation() ::_parenthesis::
| value()
parameter:
<positional_parameter>
| <named_parameter>
value:
::not:: logical_operation() #not
| <true> | <false> | <null> | <float> | <integer> | <string>
| parameter()
| variable()
| array_declaration()
| function_call()
variable:
<identifier> ( object_access() #variable_access )*
object_access:
::dot:: <identifier> #attribute_access
#array_declaration:
::bracket_:: value() ( ::comma:: value() )* ::_bracket::
#function_call:
<identifier> ::parenthesis_::
( logical_operation() ( ::comma:: logical_operation() )* )?
::_parenthesis::
</code></pre>
<p>上面Llk::load方法会加载这个基础语法内容并解析出片段tokens,tokens解析的逻辑就是正则匹配出我们需要的一些操作符和基础标识符,并将对应的正则表达式提取出来:</p>
<pre><code>array:1 [▼
"default" => array:20 [▼
"skip" => "\s"
"true" => "(?i)true"
"false" => "(?i)false"
"null" => "(?i)null"
"not" => "(?i)not\b"
"and" => "(?i)and\b"
"or" => "(?i)or\b"
"xor" => "(?i)xor\b"
"string" => "("|')(.*?)(?<!\\)\1"
"float" => "-?\d+\.\d+"
"integer" => "-?\d+"
"parenthesis_" => "\("
"_parenthesis" => "\)"
"bracket_" => "\["
"_bracket" => "\]"
"comma" => ","
"dot" => "\."
"positional_parameter" => "\?"
"named_parameter" => ":[a-z-A-Z0-9_]+"
"identifier" => "[^\s\(\)\[\],\.]+"
]
]
</code></pre>
<p>这一步也会生成一个rawRules</p>
<pre><code>array:10 [▼
"#expression" => " logical_operation()"
"logical_operation" => " operation() ( ( ::and:: #and | ::or:: #or | ::xor:: #xor ) logical_operation() )?"
"operation" => " operand() ( <identifier> logical_operation() #operation )?"
"operand" => " ::parenthesis_:: logical_operation() ::_parenthesis:: | value()"
"parameter" => " <positional_parameter> | <named_parameter>"
"value" => " ::not:: logical_operation() #not | <true> | <false> | <null> | <float> | <integer> | <string> | parameter() | variable() | array_declaration() | function_call( ▶"
"variable" => " <identifier> ( object_access() #variable_access )*"
"object_access" => " ::dot:: <identifier> #attribute_access"
"#array_declaration" => " ::bracket_:: value() ( ::comma:: value() )* ::_bracket::"
"#function_call" => " <identifier> ::parenthesis_:: ( logical_operation() ( ::comma:: logical_operation() )* )? ::_parenthesis::"
]
</code></pre>
<p>这个rawRules会通过analyzer类的analyzeRules方法解析替换里面的::表示的空位,根据$_ppLexemes属性的值,Compiler\Llk\Lexer()词法解析器会将rawRules数组每一个元素解析放入双向链表栈(SplStack)中,然后再通过对该栈插入和删除操作,形成一个包含所有操作符和token实例的数组$rules。</p>
<pre><code>array:54 [▼
0 => Concatenation {#64 ▶}
"expression" => Concatenation {#65 ▼
#_name: "expression"
#_children: array:1 [▼
0 => 0
]
#_nodeId: "#expression"
#_nodeOptions: []
#_defaultId: "#expression"
#_defaultOptions: []
#_pp: " logical_operation()"
#_transitional: false
}
2 => Token {#62 ▶}
3 => Concatenation {#63 ▼
#_name: 3
#_children: array:1 [▼
0 => 2
]
#_nodeId: "#and"
#_nodeOptions: []
#_defaultId: null
#_defaultOptions: []
#_pp: null
#_transitional: true
}
4 => Token {#68 ▶}
5 => Concatenation {#69 ▶}
6 => Token {#70 ▶}
7 => Concatenation {#71 ▶}
8 => Choice {#72 ▶}
9 => Concatenation {#73 ▶}
10 => Repetition {#74 ▶}
"logical_operation" => Concatenation {#75 ▶}
12 => Token {#66 ▶}
13 => Concatenation {#67 ▶}
14 => Repetition {#78 ▶}
"operation" => Concatenation {#79 ▶}
16 => Token {#76 ▶}
17 => Token {#77 ▶}
18 => Concatenation {#82 ▶}
"operand" => Choice {#83 ▶}
20 => Token {#80 ▶}
21 => Token {#81 ▼
#_tokenName: "named_parameter"
#_namespace: null
#_regex: null
#_ast: null
#_value: null
#_kept: true
#_unification: -1
#_name: 21
#_children: null
#_nodeId: null
#_nodeOptions: []
#_defaultId: null
#_defaultOptions: []
#_pp: null
#_transitional: true
}
"parameter" => Choice {#86 ▶}
23 => Token {#84 ▶}
24 => Concatenation {#85 ▶}
25 => Token {#89 ▶}
26 => Token {#90 ▶}
27 => Token {#91 ▶}
28 => Token {#92 ▶}
29 => Token {#93 ▶}
30 => Token {#94 ▶}
"value" => Choice {#95 ▶}
32 => Token {#87 ▶}
33 => Concatenation {#88 ▶}
34 => Repetition {#98 ▶}
"variable" => Concatenation {#99 ▶}
36 => Token {#96 ▶}
37 => Token {#97 ▶}
"object_access" => Concatenation {#102 ▶}
39 => Token {#100 ▶}
40 => Token {#101 ▶}
41 => Concatenation {#105 ▶}
42 => Repetition {#106 ▶}
43 => Token {#107 ▶}
"array_declaration" => Concatenation {#108 ▶}
45 => Token {#103 ▶}
46 => Token {#104 ▶}
47 => Token {#111 ▶}
48 => Concatenation {#112 ▶}
49 => Repetition {#113 ▶}
50 => Concatenation {#114 ▶}
51 => Repetition {#115 ▶}
52 => Token {#116 ▶}
"function_call" => Concatenation {#117 ▶}
]
</code></pre>
<p>然后返回HoaCompilerLlkParser实例,这个实例有一个parse方法,正是此方法构成了一个语法树。</p>
<pre><code>public function parse($text, $rule = null, $tree = true)
{
$k = 1024;
if (isset($this->_pragmas['parser.lookahead'])) {
$k = max(0, intval($this->_pragmas['parser.lookahead']));
}
$lexer = new Lexer($this->_pragmas);
$this->_tokenSequence = new Iterator\Buffer(
$lexer->lexMe($text, $this->_tokens),
$k
);
$this->_tokenSequence->rewind();
$this->_errorToken = null;
$this->_trace = [];
$this->_todo = [];
if (false === array_key_exists($rule, $this->_rules)) {
$rule = $this->getRootRule();
}
$closeRule = new Rule\Ekzit($rule, 0);
$openRule = new Rule\Entry($rule, 0, [$closeRule]);
$this->_todo = [$closeRule, $openRule];
do {
$out = $this->unfold();
if (null !== $out &&
'EOF' === $this->_tokenSequence->current()['token']) {
break;
}
if (false === $this->backtrack()) {
$token = $this->_errorToken;
if (null === $this->_errorToken) {
$token = $this->_tokenSequence->current();
}
$offset = $token['offset'];
$line = 1;
$column = 1;
if (!empty($text)) {
if (0 === $offset) {
$leftnl = 0;
} else {
$leftnl = strrpos($text, "\n", -(strlen($text) - $offset) - 1) ?: 0;
}
$rightnl = strpos($text, "\n", $offset);
$line = substr_count($text, "\n", 0, $leftnl + 1) + 1;
$column = $offset - $leftnl + (0 === $leftnl);
if (false !== $rightnl) {
$text = trim(substr($text, $leftnl, $rightnl - $leftnl), "\n");
}
}
throw new Compiler\Exception\UnexpectedToken(
'Unexpected token "%s" (%s) at line %d and column %d:' .
"\n" . '%s' . "\n" . str_repeat(' ', $column - 1) . '↑',
0,
[
$token['value'],
$token['token'],
$line,
$column,
$text
],
$line,
$column
);
}
} while (true);
if (false === $tree) {
return true;
}
$tree = $this->_buildTree();
if (!($tree instanceof TreeNode)) {
throw new Compiler\Exception(
'Parsing error: cannot build AST, the trace is corrupted.',
1
);
}
return $this->_tree = $tree;
}
</code></pre>
<p>我们得到的一个完整的语法树是这样的:</p>
<pre><code>Rule {#120 ▼
#_root: Operator {#414 ▼
#_name: "and"
#_arguments: array:2 [▼
0 => Operator {#398 ▼
#_name: "="
#_arguments: array:2 [▼
0 => Context {#396 ▼
#_id: "gender"
#_dimensions: []
}
1 => Parameter {#397 ▼
-name: "gender"
}
]
#_function: false
#_laziness: false
#_id: null
#_dimensions: []
}
1 => Operator {#413 ▼
#_name: "and"
#_arguments: array:2 [▼
0 => Operator {#401 ▼
#_name: ">"
#_arguments: array:2 [▼
0 => Context {#399 ▶}
1 => Parameter {#400 ▶}
]
#_function: false
#_laziness: false
#_id: null
#_dimensions: []
}
1 => Operator {#412 ▶}
]
#_function: false
#_laziness: true
#_id: null
#_dimensions: []
}
]
#_function: false
#_laziness: true
#_id: null
#_dimensions: []
}
}
</code></pre>
<p>这里有根节点、子节点、操作符参数以及HoaRulerModelOperator实例。</p>
<p>这时$executorModel = $compilationTarget->compile($ast, $context);就可以通过NativeVisitor的visit方法对这个语法树进行访问和分析了。</p>
<p>这一步走的是visitOperator()</p>
<pre><code> /**
* {@inheritdoc}
*/
public function visitOperator(AST\Operator $element, &$handle = null, $eldnah = null)
{
$operatorName = $element->getName();
// the operator does not exist at all, throw an error before doing anything else.
if (!$this->operators->hasInlineOperator($operatorName) && !$this->operators->hasOperator($operatorName)) {
throw new OperatorNotFoundException($operatorName, sprintf('Operator "%s" does not exist.', $operatorName));
}
// expand the arguments
$arguments = array_map(function ($argument) use (&$handle, $eldnah) {
return $argument->accept($this, $handle, $eldnah);
}, $element->getArguments());
// and either inline the operator call
if ($this->operators->hasInlineOperator($operatorName)) {
$callable = $this->operators->getInlineOperator($operatorName);
return call_user_func_array($callable, $arguments);
}
$inlinedArguments = empty($arguments) ? '' : ', '.implode(', ', $arguments);
// or defer it.
return sprintf('call_user_func($operators["%s"]%s)', $operatorName, $inlinedArguments);
}
</code></pre>
<p>那么编译好的规则可以通过以下方式得到:</p>
<pre><code>$executorModel->getCompiledRule()
// 规则就是 $this->unwrapArgument($target["gender"]) == $parameters["gender"] && ($this->unwrapArgument($target["points"]) > $parameters["min_points"] && $this->unwrapArgument($target["points"]) > $parameters["min_points"])
</code></pre>
<h4>自定义一个操作器</h4>
<p>由于官方文档太老且无更,所以如果你按照他的文档去自定义的话会哭晕,这里给出一个对应的示例。</p>
<pre><code class="php">$compiler = Compiler::create();
$rulerz = new RulerZ($compiler, [
new Native([
'length' => 'strlen'
],[
'contains' => function ($a, $b) {
return sprintf('strstr(%s, %s)', $a, $b);
}
])
]);</code></pre>
<p>上文中<code>contains</code>表示的是用系统函数<code>strstr()</code>来判断$a中是否包含$b字符,由于编译后的代码是通过字符串生成的,所以你在这个匿名函数中必须要用字符串表达判断逻辑,这也是其缺点之一。</p>