<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Tech on pugWoo&#39;s Life</title>
    <link>https://blog.pugwoo.com/tags/tech/</link>
    <description>Recent content in Tech on pugWoo&#39;s Life</description>
    <generator>Hugo</generator>
    <language>en</language>
    <lastBuildDate>Mon, 27 May 2024 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://blog.pugwoo.com/tags/tech/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>业务系统的标准宽表设计</title>
      <link>https://blog.pugwoo.com/posts/2024/business-system-standard-table-design/</link>
      <pubDate>Mon, 27 May 2024 00:00:00 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2024/business-system-standard-table-design/</guid>
      <description>&lt;h2 id=&#34;1-业务系统越来越复杂开发怎么分工&#34;&gt;1. 业务系统越来越复杂，开发怎么分工？&lt;/h2&gt;&#xA;&lt;h3 id=&#34;11-背景&#34;&gt;1.1 背景&lt;/h3&gt;&#xA;&lt;p&gt;公司的业务系统已经迭代了10年，子系统大大小小加起来大约有80到100个。开发团队有30多人，每人负责几个子系统，所有相关事项都由个人全权负责，需处理产品业务沟通、开发、测试、线上运维、数据统计分析等各方面的工作。当人员离职或调动时，子系统通常会交接给新人，导致文档丢失和设计意图不明确的情况屡见不鲜。随着业务的发展，公司对系统的专业性要求越来越高，专门设立了产品经理岗位；然而，开发人员的分工似乎没有太大变化。&lt;/p&gt;&#xA;&lt;p&gt;直到最近，公司开始重视数据决策，甚至请来了专业的第三方数据建模分析团队。在为数据分析团队提供数据时，我发现了一些明显的问题：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;要获取某个子领域的完整数据，需要逐一联系开发人员，询问数据的业务含义和标准，才能获取完整信息。如果要获取更多领域的完整数据，需要联系的开发会更多。&lt;/li&gt;&#xA;&lt;li&gt;数据来自多个子系统，零散分布在各自的MySQL数据库，收集、清洗、组合这些数据需要大量工作。&lt;/li&gt;&#xA;&lt;li&gt;开发系统页面功能、数据统计分析、测试、运维都是一人全包，难以在功能开发和数据分析等特定领域形成专业化的工具积累。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;总体来看，团队分工较为原始，烟囱式分配工作任务，专业深度难以提升。&lt;/p&gt;&#xA;&lt;h3 id=&#34;12-开发怎么分工&#34;&gt;1.2 开发怎么分工？&lt;/h3&gt;&#xA;&lt;p&gt;在降本增效的大背景下，公司无法在开发、测试、系统功能和数据统计等多个维度实现细化分工，因为这会增加支出。更可行的做法是在现有开发团队内进行一次细化分工。如果只能做一次一分为二的分工，我的分工思路：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;面向实际问题&lt;/strong&gt;：当前主要问题是数据获取难度大。考虑到后台开发人员的能力特点，可以将系统功能（CRUD、业务流程、页面交互接口）与数据统计分开，这两者的技术栈也有明显差异。因此，适合在这个维度上进行分工。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;常见分工方式&lt;/strong&gt;：按开发、测试、运维分工。但这种分工并不能解决主要矛盾：开发人员有能力自行测试和运维自己的功能，随着公司CI/CD和K8s部署平台的完善，这方面的矛盾并不突出。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;分析结论：将开发人员划分为“系统功能开发”和“数据统计开发”两类，各自的职能和变化点如下：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;系统功能开发&lt;/strong&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;负责子系统的CRUD、业务流程、页面接口开发。&lt;/li&gt;&#xA;&lt;li&gt;向流程引擎、消息中间件、分布式组件方向深入发展。&lt;/li&gt;&#xA;&lt;li&gt;不再负责数据统计分析，有更多时间专注于中间件开发，抽取业务系统的共性部分。&lt;/li&gt;&#xA;&lt;li&gt;以两三人小组形式负责更大范围的领域系统，合并可能重复的子系统。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;数据统计开发&lt;/strong&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;负责数据资产管理、报表开发、数据建模分析。&lt;/li&gt;&#xA;&lt;li&gt;向大数据、数据建模、数据挖掘、机器学习方向深入发展。&lt;/li&gt;&#xA;&lt;li&gt;不再处理一线业务流程的问题，减少被干扰的次数。&lt;/li&gt;&#xA;&lt;li&gt;负责比功能开发更大范围的领域，将上下游各领域数据汇总分析。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;通过这样的分工，团队可以更有效地解决当前问题，同时提升整体专业性和效率。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-分工后两类开发的边界在哪里&#34;&gt;2. 分工后，两类开发的边界在哪里？&lt;/h2&gt;&#xA;&lt;h3 id=&#34;21-数据划分&#34;&gt;2.1 数据划分&lt;/h3&gt;&#xA;&lt;p&gt;有分工，就应该有边界，明确的责任边界是顺利合作的基础。从数据的角度来看，我们需要识别整个系统中的数据，并明确每个岗位的责任。可以按照常见的业务系统数据库垂直划分和数据仓库分层划分的方法进行，如图所示。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&lt;img alt=&#34;&#34; src=&#39;.././images/standard-table-1.svg&#39; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;22-数据维护职责划分&#34;&gt;2.2 数据维护职责划分&lt;/h3&gt;&#xA;&lt;p&gt;数据维护职责划分：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;业务系统数据库，由&lt;strong&gt;系统功能开发&lt;/strong&gt;人员负责。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;数据仓库的DWS和ADS层，由&lt;strong&gt;数据统计开发&lt;/strong&gt;人员负责。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;ODS层和DWD层的职责划分存在一些争议：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;ODS层的数据是业务系统的原始数据快照，每个周期生成一次，表结构与业务系统基本一致。由于ODS层的表结构和字段含义由&lt;strong&gt;系统功能开发&lt;/strong&gt;人员设计和维护，因此由他们负责是合理的。考虑到ODS层与业务系统的耦合关系，即当业务系统的表结构发生变化时，ODS的表结构也需要相应调整，这种耦合关系由&lt;strong&gt;系统功能开发&lt;/strong&gt;人员维护再合适不过了。&lt;/p&gt;&#xA;&lt;p&gt;有了明确的责任划分后，&lt;strong&gt;数据统计开发&lt;/strong&gt;人员再也不用为了统计报表，辛苦地跑去各个业务系统数据库快照数据了。而&lt;strong&gt;系统功能开发&lt;/strong&gt;人员也不用再担心自己维护的数据库被随意导出，或者一旦修改表结构就引发其他报表统计问题。两边都省心，皆大欢喜！&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;DWD层负责对ODS层的数据进行清理和整合，形成标准的数据基础表。虽然ODS层的责任划分还算明朗，但DWD层的归属在业界和团队内部却有较大争议。&lt;strong&gt;数据统计开发&lt;/strong&gt;人员对ODS层并不熟悉，ODS层不是他们设计的，表结构也主要面向OLTP。而&lt;strong&gt;系统功能开发&lt;/strong&gt;人员对于如何设计DWD层的数据表结构也没有十足把握，需要了解清楚DWD层之上的统计分析需求才好设计，而他们并不熟悉DWD层之上的情况。&lt;/p&gt;&#xA;&lt;p&gt;究竟谁的说法更合理呢？&lt;strong&gt;数据统计开发&lt;/strong&gt;人员确实对ODS层不熟悉，不仅是表结构，明确字段的业务含义、适配表结构变更等都是难点，这符合客观事实。而&lt;strong&gt;系统功能开发&lt;/strong&gt;人员确实对DWD层之上的统计分析不了解，那么他们真的需要了解这些才能设计好DWD层的表结构吗？答案未必。从分层架构来看，上层依赖底层，底层不需要感知到上层的存在。因此，&lt;strong&gt;系统功能开发&lt;/strong&gt;人员的说法实际上站不住脚，DWD层的责任理应由他们承担。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;最终责任划分结果如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&lt;img alt=&#34;&#34; src=&#39;.././images/standard-table-2.svg&#39; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;两类开发的依赖边界是DWD层，数据统计开发人员&lt;span style=&#34;color: red;&#34;&gt;&lt;strong&gt;禁止&lt;/strong&gt;&lt;/span&gt;读写ODS层和业务系统的数据，DWD层有效隔离了业务系统表结构变更对上层的影响。为了使&lt;strong&gt;系统功能开发&lt;/strong&gt;人员可以设计出良好的DWD层，我们制定了一套《标准宽表的设计原则》。&lt;/p&gt;&#xA;&lt;h2 id=&#34;3-标准宽表的设计原则&#34;&gt;3. 标准宽表的设计原则&lt;/h2&gt;&#xA;&lt;p&gt;在介绍设计原则之前，我们先通过一个具体案例来感受一下如何设计DWD层的表结构。我们把这种符合规范的DWD层表称为“标准宽表”。&lt;/p&gt;&#xA;&lt;h3 id=&#34;31-案例公交车运营信息的宽表设计&#34;&gt;3.1 案例：公交车运营信息的宽表设计&lt;/h3&gt;&#xA;&lt;p&gt;假设你现在是一名分析员，你的长期目标是优化每辆公交车的路线和发车频率，使公交车既能更好地服务市民，又能为公司带来可观的收益。为了达成这一目标，我们需要哪些信息用于分析呢？我们希望获取的信息既要充分，不能遗漏关键信息；也不需要过多无关信息，以免增加数据获取和分析的工作量；此外，这些信息必须在当前技术水平下是可获得的。如果我们用表格来记录信息，&lt;strong&gt;每张表只能记录同一类内容&lt;/strong&gt;，至少需要以下几张表？可以进行以下场景的分析：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;哪条公交线路最拥挤？哪条线路相对空闲？&lt;/li&gt;&#xA;&lt;li&gt;哪个站点人满为患？发车频率是否合理？增加班次能否解决问题？行车路线是否需要优化？&lt;/li&gt;&#xA;&lt;li&gt;乘客等待公交车的平均时长是多少？哪条线路和哪个站点的等待人数和时间最长？&lt;/li&gt;&#xA;&lt;li&gt;哪条公交线路收益最好？什么时间段收益最好？整体收益如何？公交车型是否需要优化？&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;读者可以先花5分钟思考一下，然后再继续往下读。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;最终梳理出来，我们需要2张宽表：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;宽表1：公交车运行记录表&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;这张表格的每一行记录了一次公交出行的信息。通过它，我们可以分析线路和班次的运营情况。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段分类&lt;/th&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;示例数据及说明&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;基础信息&lt;/td&gt;&#xA;          &lt;td&gt;公交车车牌&lt;br/&gt;公交车司机&lt;br/&gt;线路&lt;br/&gt;始发站点&lt;br/&gt;结束站点&lt;/td&gt;&#xA;          &lt;td&gt;粤B6688GD&lt;br/&gt;王师傅&lt;br/&gt;101路&lt;br/&gt;火车站&lt;br/&gt;动物园总站&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;运行信息&lt;/td&gt;&#xA;          &lt;td&gt;班次&lt;br/&gt;总里程&lt;br/&gt;出发时间&lt;br/&gt;结束时间&lt;br/&gt;总载客数&lt;/td&gt;&#xA;          &lt;td&gt;08:10&lt;br/&gt;60km&lt;br/&gt;2024-01-02 08:10:00&lt;br/&gt;2024-01-02 09:52:32&lt;br/&gt;300人次&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;收入信息&lt;/td&gt;&#xA;          &lt;td&gt;车费收入&lt;br/&gt;广告收入&lt;br/&gt;补贴收入&lt;br/&gt;油费成本&lt;br/&gt;车辆成本&lt;br/&gt;司机成本&lt;/td&gt;&#xA;          &lt;td&gt;823元&lt;br/&gt;100元&lt;br/&gt;50元&lt;br/&gt;30元&lt;br/&gt;200元&lt;br/&gt;100元&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;strong&gt;宽表2：公交站点上下客记录表&lt;/strong&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>一种不错的产品方案表达方式</title>
      <link>https://blog.pugwoo.com/posts/2024/a-good-way-to-present-product-prototypes/</link>
      <pubDate>Wed, 31 Jan 2024 10:04:49 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2024/a-good-way-to-present-product-prototypes/</guid>
      <description>&lt;p&gt;我注意到，许多技术人员在介绍产品方案时，往往喜欢从技术实现开始讲解。为什么会这样呢？这可能是因为技术实现不仅展示了技术的复杂性和深度，也是技术人员获得高薪的基础。因此，从技术实现开始讲，更符合他们的思维习惯。然而，一旦我们从技术实现入手，就在一定程度上限定了产品的使用方式。&lt;/p&gt;&#xA;&lt;p&gt;大多数人对产品的使用方式，要么觉得它是理所当然的（大家都这么用，所以我的产品方案也是这样用），要么没有投入足够的思考，很少去探索是否存在更好的使用方法。然而，当我们观察这个世界时，会发现众多的技术研究和发明，归根结底都是为了让人们更好地使用功能、满足需求。因此，使用方式应该在技术实现之前进行优先考虑。为此，我梳理出了一套优先考虑产品使用方式的表达框架。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-表达框架&#34;&gt;1. 表达框架&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;背景介绍&lt;/strong&gt;（方案在怎样的环境下诞生）、&lt;strong&gt;产品目的&lt;/strong&gt;（该产品要解决什么问题？达到什么效果？）&lt;/li&gt;&#xA;&lt;li&gt;产品的&lt;strong&gt;多种使用方式设计&lt;/strong&gt;（使用者如何使用该产品），从中比较并选出使用方式最佳的方案&lt;/li&gt;&#xA;&lt;li&gt;使用方案的&lt;strong&gt;实现方式分享&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;说明：如果第2点选出的最佳方式难以实现，可以解释选择次优方式的原因是实现难度较大。&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;2-案例&#34;&gt;2. 案例&lt;/h2&gt;&#xA;&lt;p&gt;接下来，我将通过一些具体的例子来展示这种表达方式。&lt;/p&gt;&#xA;&lt;h3 id=&#34;21-分布式锁&#34;&gt;2.1 分布式锁&lt;/h3&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;背景和目的&lt;/p&gt;&#xA;&lt;p&gt;在分布式环境中，一个Java应用通常会部署多个实例。这些实例的某个函数可能会被同时调用，导致并发问题。为了确保该函数能够串行执行，我们需要一种机制来实现类似于Java单机应用中的synchronized功能。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;各种方案的使用方式：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;2.1) 代码调用的方式&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;方法中的代码里调用组件的lock(key)获取锁，调用unlock(key)释放锁，代码示例：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Resource&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; DistributedLock lock;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;someMethod&lt;/span&gt;() {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    String lockKey &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;some-key&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;boolean&lt;/span&gt; lockResult &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        lockResult &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; lock.&lt;span style=&#34;color:#a6e22e&#34;&gt;lock&lt;/span&gt;(lockKey); &lt;span style=&#34;color:#75715e&#34;&gt;// 获取锁&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (lockResult) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// 加锁成功的业务代码&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;// 加锁失败的处理代码 &lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } &lt;span style=&#34;color:#66d9ef&#34;&gt;finally&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (lockResult) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            lock.&lt;span style=&#34;color:#a6e22e&#34;&gt;unlock&lt;/span&gt;(lockKey); &lt;span style=&#34;color:#75715e&#34;&gt;// 释放锁&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;2.2) 使用注解的方式&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;方法上注解@Synchronized，代码示例：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Synchronized&lt;/span&gt;(key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;some-key&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#75715e&#34;&gt;// 当加锁失败时，抛出异常&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;someMethod&lt;/span&gt;() {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 加锁成功的业务代码&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;2.3) 方案汇总比较&lt;/strong&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>都在谈微服务，那什么是好的服务？</title>
      <link>https://blog.pugwoo.com/posts/2020/%E9%83%BD%E5%9C%A8%E8%B0%88%E5%BE%AE%E6%9C%8D%E5%8A%A1%E9%82%A3%E4%BB%80%E4%B9%88%E6%98%AF%E5%A5%BD%E7%9A%84%E6%9C%8D%E5%8A%A1/</link>
      <pubDate>Wed, 15 Jan 2020 09:39:04 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2020/%E9%83%BD%E5%9C%A8%E8%B0%88%E5%BE%AE%E6%9C%8D%E5%8A%A1%E9%82%A3%E4%BB%80%E4%B9%88%E6%98%AF%E5%A5%BD%E7%9A%84%E6%9C%8D%E5%8A%A1/</guid>
      <description>&lt;p&gt;微服务流行的这几年，都在说微服务，聊架构、聊优势，就好像没有好好聊一下什么是服务，怎样的服务算是好服务。作为一个程序员，我们会用很多第三方工具类，你会发现有些工具它面向的比较底层，它的函数很多、每个函数提供的入参也很多；有些工具比较接近业务，会提供给使用者直接能用的、很简单的函数。这些情况的存在是很正常的，底层的函数要考虑到各种各样的场景要比顶层的函数多。&lt;/p&gt;&#xA;&lt;p&gt;如果给你两个都能满足你需求的工具，一个底层一个顶层，你肯定会选择顶层。假设你现在要取消手机上的某个增值服务，你是打个官方客户电话告诉她要取消，她马上给你操作好；还是去百度搜一搜怎么取消，再根据方案一个个试，可能发个短信去取消，也可能要登录某个网站进去某个菜单去取消？显然是前者更爽。这就看得出区别了，虽然都能服务于你、满足你的需求，但有好坏之分。有的政府部门很高效，办事情直接明了；有的则在踢皮球、故意刁难；都是在为人民服务，你说有没有好坏之分呢？&lt;/p&gt;&#xA;&lt;p&gt;那么问题来了，什么样的服务是个好的服务？我认为应该是：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;能满足你的需求，这是前提&lt;/li&gt;&#xA;&lt;li&gt;你只需要表达需求，而非步骤（你不用告诉对方应该怎么做）&lt;/li&gt;&#xA;&lt;li&gt;最少的信息输入，有用的信息输出（你只需要告诉对方最少的必要的信息，对方只返回给你有用的信息）&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;当然好的服务可能成本比较高。举个例子，交通卡充值，在地铁站有两种方式：&lt;/p&gt;&#xA;&lt;p&gt;1）人工服务，表达需求是：充值。&#xA;输入是交通卡和钱。&#xA;输出是充值好的交通卡和小票&lt;/p&gt;&#xA;&lt;p&gt;2）自助服务，在机器上点击充值，根据提示放入交通卡，选择充值方式，例如微信支付，打开手机微信，扫描支付，等待机器充值完成，拿出交通卡和小票&lt;/p&gt;&#xA;&lt;p&gt;显然，如果不考虑成本或等待时间的话，人工服务对消费者来说是最好的。&lt;/p&gt;&#xA;&lt;p&gt;那如果上面的判断没有明显的差别，那还能怎么判断一个服务更好？&lt;/p&gt;&#xA;&lt;p&gt;1）客户需要操心的事情越少越好。&lt;/p&gt;&#xA;&lt;p&gt;这里包括客户的心智，例如充值服务，手工服务的话，小孩和老人都可以使用，但是自助就不一定行。&lt;/p&gt;&#xA;&lt;p&gt;操心的事情还包括：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;这个卡上次欠费了没补扣到，怎么处理？自助服务可能直接让去人工处理了&lt;/li&gt;&#xA;&lt;li&gt;这个卡读磁好像不太好，机器没识别到&lt;/li&gt;&#xA;&lt;li&gt;这个卡被机器吞了!&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;2）客户花费的时间越少越好。&lt;/p&gt;&#xA;&lt;p&gt;人工服务为什么值钱？因为它带了智能元素。它可以处理程序之外的事情。&lt;/p&gt;&#xA;&lt;p&gt;那自助服务就没有优势了吗？当然不是，它的优势就是低成本 + 易扩张 + 24小时不间断。&lt;/p&gt;&#xA;&lt;p&gt;所以，现阶段，很多都是人工+自助的形式，就是因为它们谁也替代不了对方，而最好的方式就是利用它们的优点，平衡好。&lt;/p&gt;&#xA;&lt;h3 id=&#34;2021更新&#34;&gt;2021更新&lt;/h3&gt;&#xA;&lt;h5 id=&#34;1-sql为何这么流行大数据最终也是以sql的形式来操作&#34;&gt;1. SQL为何这么流行？大数据最终也是以SQL的形式来操作。&lt;/h5&gt;&#xA;&lt;p&gt;因为SQL表达的是要什么数据（要啥就告诉啥，没有多余的信息返回）、查询什么条件就可以了，不需要告诉服务器额外的信息、也不要告诉操作过程。&lt;/p&gt;&#xA;&lt;p&gt;它符合好服务的特征。&lt;/p&gt;&#xA;&lt;h5 id=&#34;2-为何helm-charts是部署表达的最终形式&#34;&gt;2. 为何helm charts是部署表达的最终形式？&lt;/h5&gt;&#xA;&lt;p&gt;使用k8s时，我们会编写k8s的yaml进行部署，每个kind一个yaml。对于一次部署一个MySQL实例，可能需要Deployment、Service等多个kind，因此就会有多份yaml文件，因此还需要一个文件夹把这些yaml放一块。当又要部署一个MySQL实例时，又得复制出这样一堆yaml文件，可以发现，这两堆yaml文件中，有大量的信息是重复的，而它们不同的信息，恰好是最终用户部署一个MySQL实例时需要表达的信息：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;mysql的版本&#xA;root账号的密码&#xA;数据持久化路径&#xA;暴露端口&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;没有其它信息了。&lt;/p&gt;&#xA;&lt;p&gt;而helm charts就做到这种方式了，只需要用户填这几个信息，一个mysql实例就可以跑起来了。更复杂的如clickhouse、elasticsearch等都只需要用户填写用户需要指定的信息，不用做额外的事项，就可以秒起一个实例。&lt;/p&gt;&#xA;&lt;p&gt;其实我们可以发现，有些方式是最终的形式 。例如面向最终端消费者的操作界面，应该就是当前科技水平下的最终形式。如果科技水平更高，是否会出现通过人工智能、意识控制等更为最终的形式，还是可能的。&lt;/p&gt;&#xA;&lt;p&gt;在互联网形态下，web和app是普通用户的最终的展现形式。我们应该尽量将功能以这两种形式提供，而不是登录机器去敲命令或编辑复杂的配置。&lt;/p&gt;</description>
    </item>
    <item>
      <title>web服务器的线程池模型与Reactor模型</title>
      <link>https://blog.pugwoo.com/posts/2019/web%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%A8%A1%E5%9E%8B%E4%B8%8Ereactor%E6%A8%A1%E5%9E%8B/</link>
      <pubDate>Fri, 27 Dec 2019 08:55:12 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2019/web%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%A8%A1%E5%9E%8B%E4%B8%8Ereactor%E6%A8%A1%E5%9E%8B/</guid>
      <description>&lt;p&gt;这篇文章并不主要介绍线程池模型和Reactor模型的细节，而是试图从本质上区分两者的区别。&lt;/p&gt;&#xA;&lt;p&gt;首先还是要简要描述下这两者，配合一些例子说明：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;线程池模型是servlet规范确定的模型，在其它很多框架中也常见，例如dubbo。线程池模型规定一个请求由一条线程服务，直到请求执行完。为了减少创建线程的开销和限定最大线程数，一般会使用线程池来做。这意味着，如果一个请求的业务代码中，sleep了一段时间或者阻塞IO，那么这条线程也只能等当前代码处理完成，并不能被分配去处理其它请求。&lt;/p&gt;&#xA;&lt;p&gt;这就好比你去银行柜台办理业务，轮到你时，从你接受服务到结束服务，柜台服务员始终只服务于你，即便你在中途填一份文件需要一两分钟、或者接个紧急电话需要三两分钟，柜台服务员都不会在此期间去服务其它客户。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Reactor模型是异步IO复用，在nginx、haproxy等web服务器中很常见，netty框架也是此模型。Reactor模型一般使用一个线程来接入新的客户端，再使用N个线程来服务客户端，一般N是CPU核数的1到2倍，每个线程可以来回处理多个客户端的请求。&lt;/p&gt;&#xA;&lt;p&gt;如果拿上面的银行柜台办理业务场景改造一下，就变成了：有一个大堂经理，他来接待每一位新客户，然后按一定规则将客户分配给一个柜台服务员。一个柜台服务员可以同时服务多名客户：客户A向服务员提出请求，服务员处理后发给客户A一份表格让A填写；此时服务员转去处理客户B的请求；直到客户A填完表格了，喊一声服务员，服务员就过来处理客户A的表格。如此反复。&lt;/p&gt;&#xA;&lt;p&gt;上面是改造银行柜台的例子，和银行实际做法不太符合，只是为了方便大家理解过渡。再举个实际Reactor模型的例子：淘宝卖家的客服。消费者通过旺旺可以和卖家客服在线聊天，客服这边是个多客户接入系统，一次可以接入几十个客户，当她收到客户的消息时，她就会点开窗口回复客户，回复完后她就换到其它窗口回复其它客户，直到这个客户有新的消息过来。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;初略来看，可能不少人会觉得Reactor模型性能要比线程池高，支持的并发数更高。这个结论是需要在特定的条件下才成立的。所以网上流行的一些如Reactor模型性能比传统的线程池高的说法，是不够准确的。较为准确的描述应当是：Reactor可以在既定资源下发挥更大的效率。在java中，每个线程默认占1M左右的内存，每个进程最多能开2000左右的线程，再高就可能导致OOM或其它问题，因此线程是比较珍贵的资源。在线程池模型中，如果有一个请求卡住了，把默认tomcat 200个线程占满了，后面的请求就无法被处理了。这就好像银行开了10个柜台，其中办理某个业务比较慢，需要半个小时而且是卡住在客户填写资料这边，期间柜台服务员很空闲，然后刚好有20个人来办这个业务，那么这1个小时内，其他人就办理不了业务了。这就造成了一个现象：柜台服务员很空闲，外面又有很多客户没法办理业务。这个问题，就只能用Reactor模型解决。&lt;/p&gt;&#xA;&lt;p&gt;再回到技术上，以nginx为例，它就必须、也只能用Reactor模型。nginx无法控制后面代理的web服务器每个请求的处理时间，即便nginx开了2000个线程，只要有1个请求慢到10秒钟，那么200个并发就可以把nginx堵死，造成nginx无法响应其它请求，表现为僵死状态。&lt;/p&gt;&#xA;&lt;p&gt;所以，只要是网关、或者说网络请求的中间层，那必须是Reactor模型。这样当后台服务慢时，它增长的资源就只有TCP连接数，这个在操作系统层面是IO文件描述符，由linux维护，一般有epoll模型，保证处理请求的时间和资源不会随着连接数增长而暴涨。&lt;/p&gt;&#xA;&lt;p&gt;话说回来，既然Reactor优势这么明显了，那为何还有线程池模型的存在呢？既然线程池模型还在大量使用，那就说明它有它特有的优势。在线程池模型下，我们可以使用ThreadLocal，将当前请求的上下文信息等存到里面，代替层层传参的方式。此外，线程池模型下，业务逻辑的编写也比Reactor模型要简单明了很多，很大提高了开发效率。鉴于这2个优势，以线程池模型为代表的servlet规范，在Java业务开发领域，依旧是主流。&lt;/p&gt;&#xA;&lt;p&gt;最后说下结论：鉴于这两者的区别，一般的网络结构中的所有中间转发层都用Reactor模型，实际业务处理层用多线程模型。中间网络层可以有多层。其实细看tomcat的内部接口，它也是由Reactor模型（接入新客户时）+多线程模型（实际处理业务时）构成的。&lt;/p&gt;</description>
    </item>
    <item>
      <title>关于数据库主键、外键的选择</title>
      <link>https://blog.pugwoo.com/posts/2017/%E5%85%B3%E4%BA%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%BB%E9%94%AE%E5%A4%96%E9%94%AE%E7%9A%84%E9%80%89%E6%8B%A9/</link>
      <pubDate>Fri, 31 Mar 2017 15:59:10 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2017/%E5%85%B3%E4%BA%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%BB%E9%94%AE%E5%A4%96%E9%94%AE%E7%9A%84%E9%80%89%E6%8B%A9/</guid>
      <description>&lt;p&gt;在设计表时，最好为表加上一个主键id（尽量使用数字，主键索引数据最小），一种常见的做法是所有的表都有个自增主键id，其它表对该表有关联时，都关联上主键id。在实际开发过程中，这种方式是主要大多数的情况，但在一些场景下，有其麻烦的地方，需要更深入思考其本质。&lt;/p&gt;&#xA;&lt;p&gt;先假设一个场景，有一张学生表，学生有一个唯一的学号，还有姓名等其他信息。我们有两种方式来设计表：&lt;/p&gt;&#xA;&lt;p&gt;方式一，表字段：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;说明&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;id&lt;/td&gt;&#xA;          &lt;td&gt;自增id&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;student_no&lt;/td&gt;&#xA;          &lt;td&gt;学生学号，也是一个数字或字符串，也是唯一的，&lt;strong&gt;有业务含义的编号&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;name&lt;/td&gt;&#xA;          &lt;td&gt;学生姓名&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;方式二，表字段：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;说明&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;id&lt;/td&gt;&#xA;          &lt;td&gt;学生学号，主键，一个数字或字符串，也是唯一的，&lt;strong&gt;有业务含义的编号&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;name&lt;/td&gt;&#xA;          &lt;td&gt;学生姓名&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;方式一自增id的情况&#34;&gt;方式一：自增id的情况&lt;/h2&gt;&#xA;&lt;p&gt;我先直观说下自增id在哪些场景下会有麻烦的事情要处理：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;自增id在开发、测试、线上每个环境是不同的，如果代码中需要写死引用哪个id的话，由于每个环境id不同，就需要为此配置不同的id值，增加了配置项和维护工作。当然，此时配置的情况下，就应该配置为学号student_no，然后代码中再翻查出学生id。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;主键id对分表不友好，像上面的学生表，如果分表的话肯定是按学生学号来分表的，而数据库外表引用又是用主键id来引用。当分完好多表了，这些表自增id都是从1开始的，所以这些分表的id就大量相同，外表引用学生表主键id就很不好处理。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;自增id一般是系统内部的id，对外部用户是不可见的也没有业务含义。如果关联了另外一个表的id，那么展示给前端学生编号时，就需要每次靠这个内部自增id去反查学号student_no。自增id并无显著编码规则，如果关联错了字段也不易看出来，容易引用错导致数据污染。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;自增id相当于是由数据库控制的主键，应用无法自行指定其id。当发生删除数据，再添加数据时，所有原来的表外键引用都会失效，重新关联也很麻烦。例如学生都有个学校id字段，如果学校id引用的是t_school表的自增id，当该学校记录被删除，那么应用将没法（没那么容易）重新插入数据获得原来的学校id。而如果是使用编号作为主键，那么表之间的关联稳定性就会强多了。当然，这个缺点也有优点的时候，自增的id可以轻易保证不会被其它表引用，而人工指定的id则不排除该id已经被其它表引用了，导致可能的脏关联。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;方式二业务id的情况&#34;&gt;方式二：业务id的情况&lt;/h2&gt;</description>
    </item>
    <item>
      <title>一个好产品具备的要素</title>
      <link>https://blog.pugwoo.com/posts/2017/%E4%B8%80%E4%B8%AA%E5%A5%BD%E4%BA%A7%E5%93%81%E5%85%B7%E5%A4%87%E7%9A%84%E8%A6%81%E7%B4%A0/</link>
      <pubDate>Sun, 26 Feb 2017 14:30:00 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2017/%E4%B8%80%E4%B8%AA%E5%A5%BD%E4%BA%A7%E5%93%81%E5%85%B7%E5%A4%87%E7%9A%84%E8%A6%81%E7%B4%A0/</guid>
      <description>&lt;p&gt;2014年，我第一次换工作，面试了很多家公司。其中有一家公司做了一款主打产品“第一次”，它是类似朋友圈样式的微博，用户以发图片加感想文字为主，记录自己第一次做某件事情的感受。例如，第一次上大学、第一次去某个城市、第一次谈恋爱、第一次有小孩等等，每一件事情都是那么的美好，“人生若只如初见”。当时我就觉得，回忆就像一杯美酒，越久越醇香，当你再回头品味时，还是忘不了当初那份懵懂。所以，这个产品就是一个酝酿美酒的酒窖。&lt;/p&gt;&#xA;&lt;p&gt;刚上线时，该软件的用户交流还算活跃，第一次的事情总是能让人感到好奇，用户留下很多评论这问问那了解了解，甚至成为好友结伴而行。但是，这样就是一个好产品吗？我思考了好久发现这款软件有一个缺陷，它没有解决用户多大的痛点。用户因为好奇安装这个软件，看了好奇的图片和故事，心里感到很向往。这对于用户而言是闲底下打发时间的做法，无论给用户带来知识价值或者解决问题的方案都是有限的，毕竟都是新手的感言。所以，当用户有什么问题寻求解决时，几乎不会想到这个软件。也就是说，没有太多场景触发用户想起来要打开使用这款软件。&lt;/p&gt;&#xA;&lt;h3 id=&#34;1-trigger&#34;&gt;1. trigger&lt;/h3&gt;&#xA;&lt;p&gt;所以我觉得，“触发用户打开使用”是一个好产品的必要条件，我叫这个条件为trigger。基于这样的定义，我分析了一些trigger很强的产品：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;当你想知道现在自己在哪里，想坐车去某个地方时，你会打开地图类软件。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;当你想了解今天有什么事情发生时，你会打开新闻类软件。基本上大多数的资讯类产品都属于这个，当然每个人感兴趣的资讯不一样，所以这一类有很多垂直化产品。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;如果你习惯在网上充值话费，当电话费快用完了就会想起微信支付。同样，当需要取现金时，会想起把钱从微众银行转回银行卡。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;如果你刚好在等公交车，等了好久了车都没来，我想你一定非常希望有一款软件可以查下车到哪了。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;如果你经常从团购中享受周边美食的介绍和优惠，那么当你来到一个陌生的地方，你会不会打开软件看看有啥好吃有啥优惠？&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;当你想和某人联系，想拍个视频或图片给他看时，微信是不是唯一选择？&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;可以看出，trigger就是当你很想解决一个问题时所推动你去寻求解决的动力。trigger需要培养的用户习惯，当用户遇到某类问题时，脑中第一个闪过的念头就是用什么软件解决。这个效果和广告让人记住品牌有相似之处。&lt;/p&gt;&#xA;&lt;h3 id=&#34;2-linker&#34;&gt;2. linker&lt;/h3&gt;&#xA;&lt;p&gt;微信、QQ是非常成功的产品。它们满足trigger的同时，产品中用户产生的内容和信息交流都非常丰富活跃。当大部分人都使用微信时，其它类微信类的软件第一个大难题就是用户壁垒。我把这项条件称为linker。我这样定义它：用户在一款产品中的连结和互动。&lt;/p&gt;&#xA;&lt;p&gt;这类linker很强的软件，用户粘性很高。用户是这类软件的核心资产，当然用户产生的内容也是核心资产。linker本身会带动出一种强trigger，还是以QQ为例，当你和网友聊得很熟之后，你要找他，你自然会想到QQ，他要找你，他会发QQ过来，QQ消息过来本身就是一种trigger，让你去打开软件去看消息。&lt;/p&gt;&#xA;&lt;p&gt;再举一个例子，淘宝的trigger很强，当人需要在买点什么时，可能会想起网上买，而一旦想要在网上买，基本都会去淘宝搜。那它的linker在哪里呢？客户之间看起来没有太强烈的互动，实际上，淘宝上基于商品的大量成交记录和评论，就是强linker的体现。&lt;/p&gt;&#xA;&lt;p&gt;所以，如果一款产品有好的trigger和linker，那它至少满足一个好产品需要的两个前提。但是好多情况下两者很难都有，两个中有一个特别突出也会是一款不错的产品。&lt;/p&gt;&#xA;&lt;p&gt;按trigger和linker的思路看看一些产品，例如网易云音乐：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;trigger: 想听歌，网易云音乐歌曲很多很全很专业、高品质和高体验&lt;/li&gt;&#xA;&lt;li&gt;linker: 电台节目、歌单、歌曲评论、歌曲排名、个性化推荐歌曲(大数据)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;3-用户主动周期性的trigger很困难&#34;&gt;3. 用户主动周期性的trigger很困难&lt;/h3&gt;&#xA;&lt;p&gt;有类产品，帮助用户记账，一般是线下购买的东西花费的钱。只有用户将所有帐都记上，软件每个周期做的支出分析才有参考意义。问题在于，让用户养成每花一笔钱都要记一下的习惯并不容易，不排除有非常乐于记录的用户，多半是由于对支出比较在意。&lt;/p&gt;&#xA;&lt;p&gt;更困难的情况是，要让用户周期性在某个时间点自发地做一些动作，而每次做完之后又没有太大的激励。&lt;/p&gt;&#xA;&lt;p&gt;当出现这种trigger时，可以考虑降低周期，降低频次，降低用户忘记的代价，提高激励。&lt;/p&gt;&#xA;&lt;h4 id=&#34;备注&#34;&gt;备注&lt;/h4&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;截止2015-05-06，“第一次”在华为市场下载15万次，3颗星；在腾讯市场下载6万次，4.5颗星。&lt;/li&gt;&#xA;&lt;/ol&gt;</description>
    </item>
    <item>
      <title>跨域的安全性设计</title>
      <link>https://blog.pugwoo.com/posts/2016/%E8%B7%A8%E5%9F%9F%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7%E8%AE%BE%E8%AE%A1/</link>
      <pubDate>Mon, 28 Nov 2016 11:48:33 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2016/%E8%B7%A8%E5%9F%9F%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7%E8%AE%BE%E8%AE%A1/</guid>
      <description>&lt;p&gt;开发网站时，最好的设计是前端和后台在同一个域名下，这样在调用时不用涉及跨域问题，在开发、测试、预发、线上环境中切换也方便。&lt;/p&gt;&#xA;&lt;p&gt;按照这种设计，一般会这样划分域名和URL：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;m.abc.com 线上域名、预发环境域名&#xA;dev.m.abc.com 开发环境域名&#xA;test.m.abc.com 测试环境域名&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/api/    所有/api开头的，转发给后台&#xA;/     其它所有链接，转发给前端静态文件目录&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但是实际开发中，还是难免会出现跨域的问题。例如集团下两家独立子公司合作，共享登录态。于是就会涉及到跨域的设计。&lt;/p&gt;&#xA;&lt;h2 id=&#34;跨域的urlcookieajax调用相关设计&#34;&gt;跨域的URL、cookie、ajax调用相关设计&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;首先跨域调用，服务器端必须设置&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;头部。一般偷懒的做法会设置上&lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;，这样是不安全的，现代浏览器已经为这种不安全做了一定的防范：当服务器端设置的&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;是&lt;code&gt;*&lt;/code&gt;时，跨域的cookie不会发送给服务器。也即，这种方式只适合于不需要登录态的跨域调用。因此，现代浏览器的这种做法可以阻挡大部分的钓鱼冒牌网站。&lt;/p&gt;&#xA;&lt;p&gt;正常的跨域调用，显然是需要带上跨域站点的cookie的。此时就必须让服务器设置上&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;值为明确某个域名，这个可以交给浏览器根据不同的跨域请求设置不同的域名。同时javascript XHR也需要设置允许带上跨域cookie。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;为了使域名cookie在开发测试线上环境中同时可用（主要是登录态，例如微信登录就只能指定一个域名），一般我们会把cookie打在顶级域名下。但要注意外部存储平台如七牛，当七牛绑定到我们域名的某个二级域名时，请求图片就会带上顶级域名下的cookie，导致理论上七牛可以获得我们用户的登录态。解决方式是，为七牛绑定上另外一个顶级域名，例如google的所有静态资源就放在gstatic.com下，就可以从逻辑上完全杜绝google.com的cookie泄漏给CDN服务器的问题。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;</description>
    </item>
    <item>
      <title>SOA的构成要素</title>
      <link>https://blog.pugwoo.com/posts/2016/soa%E7%9A%84%E6%9E%84%E6%88%90%E8%A6%81%E7%B4%A0/</link>
      <pubDate>Fri, 15 Apr 2016 00:00:00 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2016/soa%E7%9A%84%E6%9E%84%E6%88%90%E8%A6%81%E7%B4%A0/</guid>
      <description>&lt;p&gt;SOA，面向服务架构，是构建中大型项目的必备架构，以下分析SOA的主要构成要素，并对比腾讯内部使用的SOA框架和阿里巴巴内部使用的SOA框架的优劣。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-协议&#34;&gt;1. 协议&lt;/h2&gt;&#xA;&lt;p&gt;协议的序列化性能要好，这个是肯定的，一般用google的protobuf之类的协议，有一个java protobuf协议和google的完全兼容同时不需要预先处理。&lt;/p&gt;&#xA;&lt;p&gt;协议非常重要，它负载着接口传递的信息，从扩展性和兼容性考虑都非常重要。&lt;/p&gt;&#xA;&lt;p&gt;最重要的则是协议的版本控制：&lt;/p&gt;&#xA;&lt;p&gt;1）协议有版本字段version，可以是自增也可以是时间戳，协议的每个字段会注解（或注释）是哪个版本起加入的。如果协议里面有List等更深层嵌套，如果嵌套结构不被复用，那么就可以用个自增版本控制；如果嵌入的结构是复用的，那就推荐使用时间戳作为版本。&lt;/p&gt;&#xA;&lt;p&gt;2）对于低版本客户端请求高版本服务器，高版本服务器应该兼容低版本协议，根据协议版本由服务器端来决定怎样处理业务逻辑。对于返回数据，到底是高服务器端适配低版本返回数据，还是低版本客户端忽略高版本数据字段，这个都可以考虑。对于Java自带的序列化，它对上对下都是兼容的，但没有手工指定版本那样好依靠版本做不同的业务逻辑处理。&lt;/p&gt;&#xA;&lt;p&gt;3）对于高版本客户端请求低版本服务器，低版本服务器会忽略高版本协议中新的数据，并处理业务逻辑返回低版本的数据，高版本客户端应该有能力处理低版本的返回数据。&lt;/p&gt;&#xA;&lt;p&gt;腾讯的AppServer/AppPlatform是由协议的版本来服务版本的，服务器本身没有版本号控制。服务器都是同一批升级的，没有办法控制调用某个服务时，只请求指定版本的机器。如果要实现这样的效果，就修改下服务名称，例如叫XXXv2。&lt;/p&gt;&#xA;&lt;p&gt;阿里的HSF，协议是依靠Java（Hessian）做到上下都兼容，但是没有版本区分，只能看某个字段能不能拿到值来判断，比较弱。但是服务器机器是有版本和分组的，调用方需要指定服务器版本和分组。但在实际开发过程中，线上一直是1.0.0和HSF分组，依靠版本/分组做升级的情况就没有使用过，aone2的发布也不支持部分机器1.0.0部分机器2.0.0这样的情况，所以HSF的协议版本控制是不太完善的，没有办法监控客户端和服务端的协议版本统计。当然好处就是灵活，直接Java的序列化类，不需要维护一个单独的版本。&lt;/p&gt;&#xA;&lt;p&gt;从协议版本的设计上，腾讯胜。但从实战的角度上，打平。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-注册中心与环境-本地开发测试预发线上等&#34;&gt;2. 注册中心与环境: 本地、开发、测试、预发、线上等&lt;/h2&gt;&#xA;&lt;p&gt;在实际项目开发过程中，需要搭建各种环境，至少线下线上是免不了的，测试和预发一般也是要的。SOA也需要提供这样的环境隔离。目前知道的腾讯和阿里的两种做法：&lt;/p&gt;&#xA;&lt;p&gt;1）腾讯是靠服务器上的agent来控制环境。例如有一个SOA服务，名称叫queryService，这个名称是全局唯一的，在开发环境，它对应的服务器是A:3888端口，在测试环境，它对应的服务器是B:3999端口，线上对应的是C:3888和D:3888两台机器。这些信息，可以在配置中心网页配置，配置中心以环境为单位分开配置。配置完之后，配置中心会推送给每台机器的agent程序。而对于每台机器上的应用，假如它要调SOA服务queryService，应用会拿本机的agent数据，知道要调用的SOA服务的机器端口，然后直接访问。对于本地开发，可以直接指定机器的ip端口直接调用。&lt;/p&gt;&#xA;&lt;p&gt;所以，agent维护了整个系统的环境，这个事情由运维负责。相同的代码和配置，放到A机器上它就访问了开发的服务，放到B机器上它就访问了测试的服务。这个减少了很多服务的配置项。不足是目前环境的ip需要手工配置到配置中心，实际上可以搞成自动注册的，本地环境的自动注册功能则可以关掉。&lt;/p&gt;&#xA;&lt;p&gt;2）阿里的HSF也有各种环境，但是环境里面还是需要指定HSF服务的版本号和分组（一般配置在Spring bean，这也使得每次用hsf服务都要配置，比较麻烦）。例如开发和测试环境在HSF是同一个大环境，所以应用如果依赖N个应用的服务，就需要为服务各配置N项开发和N项测试的版本。预发、线上等同理。服务提供者本身也要配置HSF版本和分组，但是会自动注册到配置中心。例如SOA服务com.xx.IQueryService，服务提供者开发环境和测试环境各部署一台服务器，开发服务器配置版本是1.0.0.daily，测试服务器配置版本是1.0.0.test，这两台服务器起来之后，会把自己的ip地址和端口自动发送给配置中心。而服务使用者的应用，如果是开发环境，那需要配置1.0.0.daily，如果是测试环境，则配置1.0.0.test，需要手工配置。&lt;/p&gt;&#xA;&lt;p&gt;这个自动注册的功能的便捷性，实际上已经被各种配置破坏完了。还好阿里是以应用为单位的，应用包含web和SOA服务层，应用上所有的服务一般都一个版本，所以像一达通后台BOSS这种规模的应用，一般十几个版本号就可以维护。&lt;/p&gt;&#xA;&lt;p&gt;其实更好的做法是一个环境自己用一个注册中心，这样就免去每个环境要用不同的版本的问题。&lt;/p&gt;&#xA;&lt;p&gt;总结一下注册中心和环境应该具备的特性：&lt;/p&gt;&#xA;&lt;p&gt;1）区分环境并感知环境的能力&#xA;2）自动负载均衡，自动下线失效机器，自动恢复上线机器&#xA;3）服务自动注册、手动注册&lt;/p&gt;&#xA;&lt;p&gt;从设计上看，腾讯的agent是非常规整的模型。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;agent应该有能力识别当前的环境，例如dev/test/pre/idc&lt;/li&gt;&#xA;&lt;li&gt;agent可以扩展为拿配置项（阿里Diamond）、mysql地址密码（保护线上mysql密码）等&lt;/li&gt;&#xA;&lt;li&gt;agent的维护有自己的门户和权限控制。如果是大公司使用时，agent可以增加命名空间来隔离不同部门。&lt;/li&gt;&#xA;&lt;li&gt;agent支持windows、linux各种操作系统。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从注册中心设计和环境使用方面，腾讯胜。&lt;/p&gt;&#xA;&lt;h2 id=&#34;3-监控治理&#34;&gt;3. 监控、治理&lt;/h2&gt;&#xA;&lt;p&gt;SOA框架会采集所有调用的耗时，调用量（分每台机器和总体），这些数据就可以做实时监控也可以做历史趋势比较。&lt;/p&gt;&#xA;&lt;p&gt;监控是简单的特性，而SOA治理才是大头。SOA应该有能力检测到从页面或定时任务开始的一个SOA调用，它后续一路下去的所有SOA调用（包括调用的服务名次、次数和时间）。通过这些数据，就可以分析调用量有没有出现循环、会不会太长、有没有良好的层次关系。这些指标可以作为系统复杂度的指导。&lt;/p&gt;&#xA;&lt;p&gt;阿里的EagleEye设计，就是通过调用链来采集数据。从页面或定时任务开始（实际上只要没有traceId，就创建一个）就创建一个随机的128位字母的traceId，此后的SOA调用或notify消息传递，都会带上这个信息。根据这个traceId，就可以找到一条调用的全过程。比扁平化的SOA监控更强大的是，这个可以对一个功能的全链路进行统计。例如用户点击提交订单的按钮后，后台会调用一系列SOA、请求一系列SQL、发送一系列信息等，哪个比较慢，一目了然。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&lt;img alt=&#34;&#34; src=&#39;https://blog.pugwoo.com/images/soa_1.png&#39; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;实际上，页面框架Webx、SOA框架HSF、消息notify、数据库TDDL这4个点都有EagleEye的采集，所以系统的全链路基本清晰了。&lt;/p&gt;&#xA;&lt;p&gt;强弱依赖统计、瓶颈分析：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&lt;img alt=&#34;&#34; src=&#39;https://blog.pugwoo.com/images/soa_2.png&#39; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;调用来源分析：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&lt;img alt=&#34;&#34; src=&#39;https://blog.pugwoo.com/images/soa_3.png&#39; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;还有个RpcId记录了调用的顺序，因为时间每台机器不一样，是不准的。&lt;/p&gt;&#xA;&lt;p&gt;Google有这类产品的设计：http://research.google.com/pubs/pub36356.html&#xA;twitter的：http://engineering.twitter.com/2012/06/distributed-systems-tracing-with-zipkin.html&lt;/p&gt;&#xA;&lt;p&gt;在SOA治理方面，阿里胜。&lt;/p&gt;&#xA;&lt;h2 id=&#34;4-管理调用方自我保护&#34;&gt;4. 管理调用方、自我保护&lt;/h2&gt;&#xA;&lt;p&gt;服务提供应该有能力保护自己。SOA的服务提供方，应该可以提供密钥和调用者标识符给调用者，并设置该调用者的调用频率，如果超过调用频率，就禁止为其提供服务并报警。&lt;/p&gt;&#xA;&lt;p&gt;另外一个是来自Google的理念，对于导致多次服务超时的异常参数，应该有屏蔽的能力。也称熔断。&lt;/p&gt;&#xA;&lt;p&gt;在这方面，目前阿里的HSF好像没有涉及，连安全性都没有。这方面，腾讯胜。&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL limit的性能和关联查询的3种写法</title>
      <link>https://blog.pugwoo.com/posts/2016/mysql-limit%E7%9A%84%E6%80%A7%E8%83%BD%E5%92%8C%E5%85%B3%E8%81%94%E6%9F%A5%E8%AF%A2%E7%9A%843%E7%A7%8D%E5%86%99%E6%B3%95/</link>
      <pubDate>Wed, 13 Apr 2016 17:14:00 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2016/mysql-limit%E7%9A%84%E6%80%A7%E8%83%BD%E5%92%8C%E5%85%B3%E8%81%94%E6%9F%A5%E8%AF%A2%E7%9A%843%E7%A7%8D%E5%86%99%E6%B3%95/</guid>
      <description>&lt;h2 id=&#34;1-mysql-limit的性能实践&#34;&gt;1. MySQL limit的性能实践&lt;/h2&gt;&#xA;&lt;p&gt;mysql的分页功能，在数据量大的情况下，即便有索引，也还是很慢。&lt;/p&gt;&#xA;&lt;p&gt;做个实验：新建一个最简单的表：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;`&lt;/span&gt;t_test&lt;span style=&#34;color:#f92672&#34;&gt;`&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;`&lt;/span&gt;id&lt;span style=&#34;color:#f92672&#34;&gt;`&lt;/span&gt; BIGINT(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt; AUTO_INCREMENT,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;PRIMARY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;KEY&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;`&lt;/span&gt;id&lt;span style=&#34;color:#f92672&#34;&gt;`&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后写个程序，往表里面插入2000万条数据，然后做下面实验：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;SQL&lt;/th&gt;&#xA;          &lt;th&gt;先执行10次后,再执行的耗时ms&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;SELECT id FROM t_test LIMIT 10000,10&lt;/td&gt;&#xA;          &lt;td&gt;3&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;SELECT id FROM t_test LIMIT 100000,10&lt;/td&gt;&#xA;          &lt;td&gt;28&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;SELECT id FROM t_test LIMIT 1000000,10&lt;/td&gt;&#xA;          &lt;td&gt;280&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;SELECT id FROM t_test LIMIT 10000000,10&lt;/td&gt;&#xA;          &lt;td&gt;4500&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;SELECT id FROM t_test LIMIT 20000000,10&lt;/td&gt;&#xA;          &lt;td&gt;6900&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;可见，即便是在只搜索索引(index索引覆盖)的情况下，只要有大的limit offset，那么查询时间和offset是O(n)线性增长。&lt;/p&gt;&#xA;&lt;p&gt;如果没有limit大offset，例如第20000000个id是20022780起，那么这样查询速度很快：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;SQL&lt;/th&gt;&#xA;          &lt;th&gt;先执行10次后,再执行的耗时ms&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;SELECT * FROM t_test WHERE id&amp;gt;=20022780 ORDER BY id LIMIT 10&lt;/td&gt;&#xA;          &lt;td&gt;1&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;SELECT id FROM t_test ORDER BY id LIMIT 10&lt;/td&gt;&#xA;          &lt;td&gt;1&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;出现上面的情况，可以感性的理解为，limit大offset对索引并没有效果，为什么没有效果？因为：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Squid的http_access访问控制顺序“一句话规则”</title>
      <link>https://blog.pugwoo.com/posts/2009/squid%E7%9A%84http_access%E8%AE%BF%E9%97%AE%E6%8E%A7%E5%88%B6%E9%A1%BA%E5%BA%8F%E4%B8%80%E5%8F%A5%E8%AF%9D%E8%A7%84%E5%88%99/</link>
      <pubDate>Mon, 21 Dec 2009 19:20:40 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2009/squid%E7%9A%84http_access%E8%AE%BF%E9%97%AE%E6%8E%A7%E5%88%B6%E9%A1%BA%E5%BA%8F%E4%B8%80%E5%8F%A5%E8%AF%9D%E8%A7%84%E5%88%99/</guid>
      <description>&lt;p&gt;Squid是搭建强大的正向代理服务器和反向代理服务器软件。所有设置说明均在官方Wiki书籍中译本：&lt;a href=&#34;http://home.arcor.de/mailerstar/jeff/squid/&#34;&gt;Squid权威指南&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;p&gt;Squid配置文件squid.conf中用acl定义控制列表是顺序无关的，但http_access设置访问控制则与顺序密切相关。初用Squid，被设置顺序搞得很疑惑，于是想出&amp;quot;一句话规则&amp;quot;来理解。&lt;/p&gt;&#xA;&lt;p&gt;http_access可以设置很多类型的允许(allow)/禁止(deny)访问。主要的类型有：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;src ：源地址，即连接该代理服务器的主机的IP地址或地址段&lt;/li&gt;&#xA;&lt;li&gt;dst ：目标地址，即连接该代理服务器的主机欲访问的IP地址或地址段&lt;/li&gt;&#xA;&lt;li&gt;dstdomain ：目标域名，即连接该代理服务器的主机欲访问的域名&lt;/li&gt;&#xA;&lt;li&gt;time ：时间&lt;/li&gt;&#xA;&lt;li&gt;其它详见&lt;a href=&#34;http://blog.s135.com/book/squid/chap06.html&#34;&gt;访问控制&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;每一次客户端请求代理服务器都会产生组合（源地址src，目标地址dst，目标域名dstdomain，时间time，等等&amp;hellip;.），服务器端拿着此组合，&lt;strong&gt;按http_access的顺序进行匹配（只匹配相同的类型），直到第一次匹配到allow或deny的http_access，如果是allow，则允许访问，不再匹配；如果是deny，则禁止访问，不再匹配。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;注意：如果一条组合匹配到最后都没有匹配到allow或deny，那么如果它最后进行匹配的http_access是allow，则deny此次请求；否则allow此次请求。&lt;/p&gt;&#xA;&lt;p&gt;还有，如果http_access allow后面有两个及以上的值，那么唯当这些值都为真，该请求才能被allow。&lt;/p&gt;&#xA;&lt;p&gt;例子：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;只允许192.168.1.0/24子网访问，但不允许访问cctv.com&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;      acl all src all&#xD;&#xA;      acl allowip src 192.168.1.0/24&#xD;&#xA;      acl badsite dstdomain www.cctv.com&#xD;&#xA;      http_access deny badsite&#xD;&#xA;      http_access allow allowip&#xD;&#xA;      http_access deny all&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当非此子网访问时，遇到最后一条deny all会匹配源IP地址，被deny掉，注意，前面两条控制并未匹配到。当该子网访问但未访问cctv.com时，遇到allow allowip被allow放行。当有人访问cctv.com，无论是不是此子网，它都被deny badsite这条规则deny掉。&lt;/p&gt;&#xA;&lt;p&gt;注意，如果allowip和badsite这两条规则调换就阻止不了访问cctv.com了。&lt;/p&gt;&#xA;&lt;ol start=&#34;2&#34;&gt;&#xA;&lt;li&gt;所有人允许访问google.com，不允许访问其它网站&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;      acl all src all &#xD;&#xA;     acl goodsite dstdomain .google.com&#xD;&#xA;     http_access allow goodsite&#xD;&#xA;     http_access deny all&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;按照一句话规则解释得通。&lt;/p&gt;&#xA;&lt;p&gt;如果要控制上网时间，则上网时间应该设置在子网访问控制的前面，否则就无法控制时间了。&lt;/p&gt;&#xA;&lt;p&gt;可以得出，&lt;strong&gt;控制范围越小的放在越前面，allow和deny构成成一对来控制。&lt;/strong&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>OpenGL中的glLoadIdentity、glTranslatef、glRotatef原理</title>
      <link>https://blog.pugwoo.com/posts/2009/opengl%E4%B8%AD%E7%9A%84glloadidentitygltranslatefglrotatef%E5%8E%9F%E7%90%86/</link>
      <pubDate>Sun, 29 Nov 2009 02:47:37 +0000</pubDate>
      <guid>https://blog.pugwoo.com/posts/2009/opengl%E4%B8%AD%E7%9A%84glloadidentitygltranslatefglrotatef%E5%8E%9F%E7%90%86/</guid>
      <description>&lt;p&gt;关于这几个函数的文章比比皆是，这是OpenGL的入门函数，但我想我讲的将不同于目前网上的文章。&lt;/p&gt;&#xA;&lt;p&gt;OpenGL中的坐标用&lt;strong&gt;齐次坐标&lt;/strong&gt;表示，即&lt;code&gt;(x,y,z)&lt;/code&gt;表示成&lt;code&gt;(x&#39;,y&#39;,z&#39;,h)&lt;/code&gt;，其中&lt;code&gt;x=x&#39;/h; y=y&#39;/h; z=z&#39;/h&lt;/code&gt;. 通常h取1. 比如空间中的点&lt;code&gt;(2,3,4)&lt;/code&gt;，在OpenGL中将表示成&lt;code&gt;(2,3,4,1)&lt;/code&gt;. 齐次坐标表示方式适合于矩阵运算，也很方便地表示了无穷远的点，比如(1,0,0,0)就表示x轴上无穷远的点，因为1/0是无穷大，这里约定0/0=0.&lt;/p&gt;&#xA;&lt;p&gt;接着要说点矩阵(线性代数)的知识。OpenGL里面的平移、旋转、缩放等变换均是线性变换，用矩阵相乘来表示。以平移变换为例，请见官方对&lt;a href=&#34;http://www.opengl.org/sdk/docs/man/xhtml/glTranslate.xml&#34;&gt;glTranslatef函数&lt;/a&gt;的说明。假设有点(3,3,3)，如果把该点沿x轴移动2单位，沿y轴移动3单位，沿z轴移动4单位，那么该点会是&lt;code&gt;(3+2, 3+3, 4+4) = (5,6,7)&lt;/code&gt;. 用矩阵表示是：&lt;/p&gt;&#xA;&lt;img src=&#34;http://farm3.static.flickr.com/2632/4143575940_1d29257e93.jpg&#34; alt=&#34;2009-11-29 18-42-47.png&#34; height=&#34;80&#34; width=&#34;197&#34;/&gt;&#xD;&#xA;&lt;p&gt;左边的矩阵称为平移变换矩阵，若把2、3、4换成x、y、z，则用它乘以一个齐次坐标表示的向量，就可以将该向量平移(x,y,z). &lt;a href=&#34;http://www.opengl.org/sdk/docs/man/xhtml/glRotate.xml&#34;&gt;旋转变换&lt;/a&gt;和缩放变换都像平移变换一样可用一个矩阵来表示。这里可以不用理会这些矩阵长什么样，只需清楚它们乘以一个齐次坐标表示的向量，就可以使该向量发生需要的变换。&lt;/p&gt;&#xA;&lt;p&gt;把平移变换矩阵记为&lt;code&gt;T(x,y,z)&lt;/code&gt;，旋转变换矩阵记为&lt;code&gt;R(x,y,z,s)&lt;/code&gt;，表示绕向量(x,y,z)旋转s角度；把向量记为X。这里只需要知道它们是矩阵就行了，现在要把一个点X，如(3,3,3,0)，移动(2,2,2)单位，再绕y轴旋转30度角，用矩阵表示即&lt;code&gt;R(0,1,0,30)*T(2,2,2)*X&lt;/code&gt;，可以理解为离X最近的矩阵最先作用。理解这个顺序很重要，这样，所有变换都可以用一串矩阵的相乘来表示，计算机里面也确实是这么做的。&lt;/p&gt;&#xA;&lt;p&gt;介绍完基本的数学知识，下面说OpenGL的作用机制。OpenGL有个变换矩阵堆栈，堆栈就像子弹夹一样，先进的后出。OpenGL中的每个向量，在被定义之后进入到OpenGL世界中，都必须先乘以这个变换矩阵堆栈的栈顶变换矩阵。如下图所示：&lt;/p&gt;&#xA;&lt;img src=&#34;http://farm3.static.flickr.com/2581/4143576202_143d0222ec.jpg&#34; alt=&#34;2009-11-29 19-12-50.png&#34; height=&#34;227&#34; width=&#34;329&#34;/&gt;&#xD;&#xA;&lt;p&gt;理解完上面的知识，再来理解glLoadIdentity、glTranslatef、glRotatef这些函数干了什么就容易多了。这些函数就是对这个堆栈的操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;glTranslatef：将T(x,y,z)右乘与堆栈的栈顶变换矩阵。右乘的解释，假设目前栈顶变换矩阵为M，那么就相当于把M修改为M*T.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;glRotatef ：将R(x,y,z,s)右乘与堆栈的栈顶变换矩阵。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;glLoadIdentity：将堆栈的栈顶变换矩阵设置成单位矩阵。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;glPushMatrix：将堆栈的栈顶变换矩阵复制一份，然后Push到堆栈中。所谓Push，就像塞子弹一样把一个矩阵压入到堆栈中，此时，栈顶就是这个新的矩阵了，注意定义的向量都是和栈顶变换矩阵作用的。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;glPopMatrix：将堆栈的栈顶变换矩阵Pop出来。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;该讲的讲完了，下面出几道题目练习下吧。&lt;/p&gt;&#xA;&lt;p&gt;1、OpenGL代码是：glLoadIdentity(); glTranslate3f(4,5,1); glRotate3f(0,1,0,90); glVertex3f(1,1,1); 请问此时栈顶变换矩阵是什么？(1,1,1)这个点到了OpenGL世界中的点是什么？&lt;/p&gt;&#xA;&lt;p&gt;答：栈顶变换矩阵是&lt;code&gt;T(4,5,1)*R(0,1,0,90)&lt;/code&gt;，(1,1,1)到OpenGL世界中的坐标是&lt;code&gt;T(4,5,1)*R(0,1,0,90)*(1,1,1)&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;2、解释为什么使用glPushMatrix和glPopMatrix的组合可以隔离这两个函数中的变换，使之不影响后面的点？&lt;/p&gt;&#xA;&lt;p&gt;答：glPushMatrix新压入的变换矩阵是复制了原来的栈顶变换矩阵，所以它继承了之前的变换，此后执行glTranslatef、glRotatef这些函数时，修改的是栈顶变换矩阵，在glPopMatrix之前的点都将受到栈顶变换矩阵的作用，之后用glPopMatrix，把栈顶变换矩阵Pop掉，此时的栈顶变换矩阵又还原成原来的那个栈顶变换矩阵。&lt;/p&gt;&#xA;&lt;p&gt;3、为什么有时候glTranslate3f和glRotate3f能颠倒有时候又不能？&lt;/p&gt;&#xA;&lt;p&gt;答：矩阵A乘以矩阵B未必等于矩阵B乘以矩阵A，当它们有相等的时候，很多时候这只是巧合。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
