<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>alsotang (alsotang)</title>
    <link>https://ruby-china.org/alsotang</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>一个简单的 MySQL 队列问题</title>
      <description>&lt;p&gt;最近有个朋友要实现队列任务方面的工作，我们就 mysql(innodb) 的事务和锁的特性聊了一些有趣的话题。&lt;/p&gt;

&lt;p&gt;其中，最终的解决方案来自大神 &lt;a href="https://github.com/fengmk2" rel="nofollow" target="_blank"&gt;https://github.com/fengmk2&lt;/a&gt; 之前的一个队列实现。
我做了一个小改进，使得之前表级锁的表现可以恢复到行级锁水平。&lt;/p&gt;

&lt;p&gt;任务的大致描述是这样的：&lt;/p&gt;

&lt;p&gt;有一个表，里面存了很多的用户 id，大概 100w 条，表的结构简化如下：&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;user_block_status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="err"&gt;用户的&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="err"&gt;用户的状态。&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;
  &lt;span class="n"&gt;updated_time&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="err"&gt;更新时间戳&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个表里面，每隔 10 秒就要去检查用户是否存在违规页面。如果存在的话，则需要把 status 置为 2，默认是 1。&lt;/p&gt;

&lt;p&gt;有 100 个 worker 会并发地从表里面读取 user_id，所以我们要设计一个策略，使得这 100 个 worker 在并发时，
读到的是独立的 100 个条目。&lt;/p&gt;
&lt;h2 id="方案1"&gt;方案 1&lt;/h2&gt;
&lt;p&gt;一开始的方案是这样的：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;这一句不一定会发请求&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;可能会优化成跟接下来的第一个&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;一起发出&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin_transaction&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;第一次io发生&lt;/span&gt;&lt;span class="err"&gt;。&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;如果一个用户在&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;内没有被更新&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;那么取出来&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;这时候由于程序拿得到&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;的值&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;所以网络io是发生了的&lt;/span&gt;&lt;span class="err"&gt;。&lt;/span&gt;&lt;span class="n"&gt;否则拿不到&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;的值&lt;/span&gt;
&lt;span class="n"&gt;outdate_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;select user_id where updated_time &amp;lt; ? order by updated_time asc limit 1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;第二次&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt; &lt;span class="n"&gt;发生&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;更新这一行的&lt;/span&gt; &lt;span class="n"&gt;updated_time&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;免得被其他worker重复读取&lt;/span&gt;
&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;update user_block_status set updated_time=now() where user_id = ?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;第三次&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt; &lt;span class="n"&gt;发生&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;do&lt;/span&gt; &lt;span class="n"&gt;something&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这个地方我们发起了 3 次 io 请求。当然，请求数不是很关键，因为请求数以及对应的时间是一个恒定量，
而随着 worker 的增加，这一块并不会带来额外的性能瓶颈。但由于我们使用了事务，所以当 worker 由 100 增加到
1000 的时候，数据库由于存在大量的事务操作，这些事务都需要掌握写锁，所以有潜在的写锁排队问题。&lt;/p&gt;

&lt;p&gt;而且关键是，方案是不可行的，根本没有起到队列的效果。&lt;/p&gt;

&lt;p&gt;为什么呢？我们假设网络 io 无限快，而数据库每条语句的执行时间是 1s，那么我们这个事务的执行时间是 2s。
这时如果 3 个 worker 并发地在同一秒（00:00）执行，那么假设 worker1 读到的 user_id 是 10086，
由于读锁是共享的，worker2 和 worker3 读到的 user_id 也是 10086。这时他们三个都想要更新 10086 的值，
而 worker1 抢先加了写锁，所以 worker2 和 worker3 就需要等待 worker1 的事务执行完毕，
才能重新获得 10086 的写锁并进行写入。
所以当 worker2 执行的时候，是 00:02 的时候，当 worker3 执行的时候，是 00:04 的时刻。
而且由于他们都是在对 10086 进行更新，所以没有起到队列的效果。&lt;/p&gt;

&lt;p&gt;这里的查询条件太特殊，导致所有并发的事务需要的都是同一条数据，
这时候 innodb 行级锁的特性也没有发挥出来。&lt;/p&gt;

&lt;p&gt;这个方案不仅并发时的表现类似表级锁的特性，而且也没有达到队列的效果。&lt;/p&gt;
&lt;h2 id="方案2"&gt;方案 2&lt;/h2&gt;
&lt;p&gt;将 update 语句在先，select 语句在后。&lt;/p&gt;

&lt;p&gt;update 语句改成&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;update user_block_status set updated_time=now() where updated_time &amp;lt; ?   order by updated_time asc limit 1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;## each worker can get different result.user_id
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在 update 的时候，3 个 worker 会排队，分别更新不同的 user_id 条目。然后返回来的
也是不同的 user_id。&lt;/p&gt;

&lt;p&gt;可关键是，update 语句并不会将被 update 了的 id 返回给程序，所以我们后面的 select 语句拿不到对应的 user_id。
这个方案先否决。&lt;/p&gt;
&lt;h2 id="方案3"&gt;方案 3&lt;/h2&gt;
&lt;p&gt;方案 1 的基础上，在 select 语句中，手工地干扰一下，使得不同的 worker 取到不同的条目&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="n"&gt;random_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;random_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;worker_count&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;select user_id where updated_time &amp;lt; ?  order by updated_time asc limit 1 offset ?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_number&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时，我们的 worker 有很大的几率可以取出不同 user_id。但这里也还有个问题就是，很可能两个 worker 的
random_number 是同一个值。那么就发生了两次重复读取，不过对于我们的业务来说，重复读取只会造成资源的浪费，
而不会带来数据一致性的问题。只要尽量减少重复读的几率，那么这个方案就是可被接受的。&lt;/p&gt;

&lt;p&gt;其中 worker_count * 2 是拍脑袋决定的数，如果数据库中始终有大量需要处理的数据，可以加大点。&lt;/p&gt;
&lt;h2 id="方案4"&gt;方案 4&lt;/h2&gt;
&lt;p&gt;方案 3 还是挺不完美的，虽然能解决问题，但是从概念上来说，我们需要的是队列。
队列的意思就是：排队！排队！排队！&lt;/p&gt;

&lt;p&gt;方案 3 只是从业务逻辑层面出发，做出了一些规避，模拟了我们需要的效果。&lt;/p&gt;

&lt;p&gt;那么回到方案 2，其实方案 2 是更接近队列的。因为不同的 worker 真正在等待另一个 worker 更新东西。
可方案 2 无奈的是，我们拿不到被更新的 id。那么有没有办法拿到呢？&lt;/p&gt;

&lt;p&gt;其实是有的，用 mysql 的 LAST_INSERT_ID() 函数。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LAST_INSERT_ID():  Value of the AUTOINCREMENT column for the last INSERT&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;关于这个函数可以看看 &lt;a href="https://dev.mysql.com/doc/refman/5.7/en/information-functions.html" rel="nofollow" target="_blank"&gt;https://dev.mysql.com/doc/refman/5.7/en/information-functions.html&lt;/a&gt; 这里的详细介绍。&lt;/p&gt;

&lt;p&gt;这个函数本来的含义是，拿到 AUTO_INCREMENT 那一列的最新值。也就是我们最新 insert 进表的那个 id。
但实际上，它也可以作为一个 sql 语句中的变量来使用，它可以被赋值，然后取出。
而且它的作用域是同一 connection 内，这样我们多个 worker 如果对 LAST_INSERT_ID 赋了不同的值，
也不会互相干扰，因为不同的 worker 使用不同的 connection。&lt;/p&gt;

&lt;p&gt;这时，我们的查询在方案 2 的基础上就变成：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin_transaction&lt;/span&gt;

&lt;span class="n"&gt;outdate_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;update user_block_status set updated_time=now()，
 id=LAST_INSERT_ID(id) where updated_time &amp;lt; ?  order by updated_time asc limit 1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;select user_id where id = LAST_INSERT_ID()&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;## do sth with line.user_id
&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ok，已经能排队了，业务上已经可以满足了。&lt;/p&gt;

&lt;p&gt;目前性能上说，网络 io 还是三个，而且，【行级锁】没有被利用的特定依然存在。
写锁依然要排队，为什么这么说？因为不管 worker 有多少个，当他们并发的时候，where 条件都始终把它们
指向同一行数据，所以还是要为了同一行数据排队。即使目前我们已经达成了【排队之后，互相更新不同条目】这个目的。&lt;/p&gt;

&lt;p&gt;方案 4 就总的性价比来说，目前跟方案 3 相比，还不一定谁好谁坏。
方案 4 的性能在于多个 worker 抢一个锁，大家总是等；方案 3 是无脑乱取，造成资源浪费，降低 worker 的效率，浪费机器。&lt;/p&gt;

&lt;p&gt;什么情况下方案 3 好？
如果总是有一大堆数据没有被处理的话，那么把方案 3 的乱取范围开大点，就能更好避免浪费。
而当一大堆数据等待处理的时候，方案 4 却不停在排队，这就等于堵住了。&lt;/p&gt;

&lt;p&gt;还有一种情况就是，方案 4 的写锁排队已经成为瓶颈。但其实这跟上面是一回事，当总是有一大堆 worker 来取
东西的话，说明就是有一大堆数据没有被处理。否则开那么多 worker 干嘛。&lt;/p&gt;

&lt;p&gt;什么情况下方案 4 好？
前提就是，写锁排队并不成为瓶颈。如果要处理的数据并不是那么多，那么使用方案 4 的话，可以降低我们需要的 worker 数量，节约机器。
而且 worker 数量评估可以更加理性。&lt;/p&gt;
&lt;h2 id="方案5"&gt;方案 5&lt;/h2&gt;
&lt;p&gt;那么，我们把方案 3 的 offset 思想加进来吧。可惜啊可惜，update 语法只支持 limit，不支持 offset。&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LOW_PRIORITY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;IGNORE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;table_reference&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;col_name1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expr1&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="k"&gt;DEFAULT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="n"&gt;col_name2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expr2&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="k"&gt;DEFAULT&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;where_condition&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那就绕一绕。&lt;/p&gt;

&lt;p&gt;不用 offset，而是通过更改 outdate_time 的值，让他们获得不同的行数据。&lt;/p&gt;

&lt;p&gt;我们的程序是要求 10s 算作过期，那么 11s、20s、30s 肯定也算过期吧。那就这样写：&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;在&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="n"&gt;到&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;之间随机取值&lt;/span&gt;
&lt;span class="n"&gt;outdate_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;update user_block_status set updated_time=now()，
 id=LAST_INSERT_ID(id) where updated_time &amp;lt; ?  order by updated_time asc  limit 1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;where updated_time &amp;lt; now() - 10s&lt;/code&gt; 与 &lt;code&gt;where updated_time &amp;lt; now() - 12s&lt;/code&gt; 与 &lt;code&gt;where updated_time &amp;lt; now() - 15s&lt;/code&gt;（不要在 where 条件里面写计算，这只是示例）
还是有可能锁定同一条数据。但至少，这个方案既利用上了行级锁，也不会造成多个 worker 处理同一 user_id 的
资源浪费。&lt;/p&gt;
&lt;h2 id="方案6"&gt;方案 6&lt;/h2&gt;
&lt;p&gt;锁的问题差不多就这么解决了。&lt;/p&gt;

&lt;p&gt;我们再回头看看，发现还有个 io 问题可以再弄弄。现在还是 3 个 io 嘛。&lt;/p&gt;

&lt;p&gt;其实到了现在这步，&lt;/p&gt;

&lt;p&gt;begin_transaction 可以去掉了。因为我们只有一个涉及写锁的操作在里面，这个操作本身作为单一语句，
就已经是原子性的了。&lt;/p&gt;

&lt;p&gt;但由于我们利用了 LAST_INSERT_ID，所以我们要保证 update 语句和它之后的 select 语句在同一个 connection 中。&lt;/p&gt;

&lt;p&gt;很多的 mysql 库实现都是用了连接池的，所以同一段代码中的两条 sql 有可能会利用两条 connection，
导致得到我们非预期的 user_id。&lt;/p&gt;

&lt;p&gt;但就我们的业务来说，LAST_INSERT_ID 混了其实是没关系的。每个 worker 始终还是会得到一个 unique 的 user_id。
这就够了。那么我们也不必加一些多余的逻辑，保证这两条语句取到同一个 connection。&lt;/p&gt;

&lt;p&gt;这时，io 操作从 3，降低到了 2。&lt;/p&gt;

&lt;p&gt;那么，有没有可能降到 1 呢。&lt;/p&gt;

&lt;p&gt;其实也可以啊............因为基本所有 mysql 库都支持 multistatements 特性。&lt;/p&gt;

&lt;p&gt;我们可以在一条 query 写两个语句，返回接口会是一个数组，分别表示这两个语句的值。&lt;/p&gt;

&lt;p&gt;类似这样，&lt;code&gt;sql.query('update .....; select ....;')&lt;/code&gt;。这是支持的。而且这么一来，
同一 connection 的问题也解决了。避免为以后留坑。&lt;/p&gt;
&lt;h2 id="重写方案"&gt;重写方案&lt;/h2&gt;&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;update user_block_status set updated_time=now()，
 user_id=LAST_INSERT_ID(user_id) where updated_time &amp;lt; ?  order by updated_time asc limit 1;

 select * from user_block_status where user_id = LAST_INSERT_ID()&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;do&lt;/span&gt; &lt;span class="n"&gt;something&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;。。。。。。。。。。。。。&lt;/p&gt;

&lt;p&gt;还是有坑的。。。。。。。。。。。。。。。&lt;/p&gt;

&lt;p&gt;如果 &lt;code&gt;where updated_time &amp;lt; ?&lt;/code&gt; 一条都不命中，那么会发生什么结果？&lt;/p&gt;

&lt;p&gt;首先，update 没有改变任何行。而 LAST_INSERT_ID 还是会返回一个合理的 id，有可能是真正的 LAST_INSERT_ID，
也可能是这条 connection 中上次手工设置的。&lt;/p&gt;

&lt;p&gt;在这里可以多说一下 LAST_INSERT_ID 的特性。默认情况下，LAST_INSERT_ID() 不带参数会返回最新插入那条的 id。
带参数的情况下 LAST_INSERT_ID(id) 本身的返回值就是参数，然后在接下来的调用中，如果不发生任何 insert，那么
值会在 connection 中一直保持。如果发生了 insert，就会被更新。&lt;/p&gt;

&lt;p&gt;如果不处理这个 update nothing 的异常情况，当队列全部被处理完的时候，
我们的 worker 会一直工作，不会停下来。所以我们要在取 LAST_INSERT_ID 的值时，
判断一下上一条 update 语句到底有没有发生作用。&lt;/p&gt;

&lt;p&gt;这时候我们需要用到另一个跟 LAST_INSERT_ID 一起出现在文档中的函数，&lt;/p&gt;

&lt;p&gt;ROW_COUNT():    The number of rows updated&lt;/p&gt;

&lt;p&gt;判断一下 ROW_COUNT，如果是 0 的话，就条件不符，这时候我们在程序里面拿到的值就是空。&lt;/p&gt;
&lt;h2 id="最终方案"&gt;最终方案&lt;/h2&gt;&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;update user_block_status set updated_time=now()，
  user_id=LAST_INSERT_ID(user_id) where updated_time &amp;lt; ?  order by updated_time asc limit 1;

  select * from user_block_status where user_id = LAST_INSERT_ID()
    and ROW_COUNT() &amp;lt;&amp;gt; 0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;outdate_time&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;do&lt;/span&gt; &lt;span class="n"&gt;something&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，mysql 用来解决这种队列问题可能不是一个好的方案。队列相关的知识，我还在努力学习中。&lt;/p&gt;
&lt;h2 id="参考资料："&gt;参考资料：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="http://www.cnblogs.com/zhoujinyi/p/3437475.html" rel="nofollow" target="_blank"&gt;http://www.cnblogs.com/zhoujinyi/p/3437475.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/5.7/en/information-functions.html" rel="nofollow" target="_blank"&gt;https://dev.mysql.com/doc/refman/5.7/en/information-functions.html&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>alsotang</author>
      <pubDate>Sun, 25 Oct 2015 03:30:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/27814</link>
      <guid>https://ruby-china.org/topics/27814</guid>
    </item>
    <item>
      <title>Web 开发后端缓存思路</title>
      <description>&lt;p&gt;原帖在这里：&lt;a href="https://cnodejs.org/topic/55210d88c4f5240812f55408" rel="nofollow" target="_blank"&gt;https://cnodejs.org/topic/55210d88c4f5240812f55408&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我觉得这个内容不区分语言，所以就转过来了。&lt;/p&gt;

&lt;p&gt;Web 应用是个典型的 io 数据流，&lt;/p&gt;

&lt;p&gt;&lt;img src="//dn-cnode.qbox.me/Fj3T_sqRzr7IV3RNLfiuf81tQuPx" title="" alt="QQ20150405-4.png"&gt;&lt;/p&gt;

&lt;p&gt;首先，浏览器发来一个 input，服务器获取之后，做一些查询或者计算，然后把生成的 output 返回给浏览器。&lt;/p&gt;

&lt;p&gt;这些查询或计算，还会有衍生的子 io 流。&lt;/p&gt;

&lt;p&gt;缓存的目的就是让把 input 变成一个 key，在条件允许的情况下，跳过计算，直接生成 output。在主流程中，或子流程中。&lt;/p&gt;
&lt;h2 id="数据查询缓存"&gt;数据查询缓存&lt;/h2&gt;
&lt;p&gt;resource: &lt;a href="http://robbinfan.com/blog/3/orm-cache" rel="nofollow" target="_blank"&gt;http://robbinfan.com/blog/3/orm-cache&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="n + 1 问题"&gt;n + 1 问题&lt;/h3&gt;
&lt;p&gt;n + 1 问题是 orm 竟然被诟病的地方。什么是 n + 1 问题呢？
比如一个用户，它写了 20 篇博客。当我们查询这个用户的首页时，需要列出他的所有博客。
“高效”的思路是使用一个 join 语句，把 user 表和 blog 表做 join，然后一条语句取出所有想要的字段。
而 orm，会先取出 user 的记录，再做 20 次遍历，分别用 20 条语句取出他所有的博客。
按照 robbin 的说法，join 语句的结果很难被缓存利用，因为它发生的场景太过特定。
但如果使用 orm，按照 n + 1 的方式取数据。由于数据缓存的粒度比较小，缓存的命中率得到了提高。
首先，orm 内置的缓存一般会在同一个连接中，缓存同一 sql 语句的结果；其次，数据库的缓存会记下特定 sql 语句的对应的结果，当再次收到相同语句时，数据库不必进行扫描，可以直接 O(1) 复杂度地返回缓存结果。&lt;/p&gt;

&lt;p&gt;robbin 认为，在这种情况下，n + 1 的查询反而因为有效利用了缓存，而比 join 语句更快。&lt;/p&gt;

&lt;p&gt;robbin 得出了这样的结论：即使不使用对象缓存，ORM 的 n+1 条 SQL 性能仍然很有可能超过 SQL 的大表关联查询，而且对数据库磁盘 IO 造成的压力要小很多&lt;/p&gt;
&lt;h3 id="缓存层加入"&gt;缓存层加入&lt;/h3&gt;
&lt;p&gt;利用 redis 或者 memcached，这个话题 google 一下会有很多。&lt;/p&gt;
&lt;h3 id="json to orm 问题"&gt;json to orm 问题&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://cnodejs.org" rel="nofollow" target="_blank" title=""&gt;CNode&lt;/a&gt; 使用的是 mongoose 这个 odm 来访问 mongodb。在 mongoose 的 model 中，我们定义了不少【虚拟属性】，所谓虚拟属性，就是指：一个 user 实例，它有 &lt;code&gt;first_name&lt;/code&gt; 和 &lt;code&gt;last_name&lt;/code&gt; 字段，当我们定义一个名字为 &lt;code&gt;full_name&lt;/code&gt; 的虚拟属性时，user.full_name 会根据定义的函数自动拼接 first_name 和 last_name。也就是面向对象编程中的 getter 方法。&lt;/p&gt;

&lt;p&gt;当缓存一个 mongoose 取出的文档到 redis 时，我们会将它先装换成 json，再以字符串形式存入。
再次取出并 JSON.parse 的时候，会发现 mongoose model 定义的虚拟属性全都被丢弃了。所以这时，需要重新把这个 json 传入 model 初始化一次，得到一个 model 实例。这样，我们就恢复了原来内存中的那个 model 实例了。&lt;/p&gt;
&lt;h2 id="数据写入缓存："&gt;数据写入缓存：&lt;/h2&gt;&lt;h3 id="在数据库与服务端之间利用 redis"&gt;在数据库与服务端之间利用 redis&lt;/h3&gt;
&lt;p&gt;这是一个很常见的场景。比如文章的浏览数，每次文章被浏览时，浏览数都 +1。如果每次都回写数据库，不免数据量太大。加上数据库看似简单，其实做了不少关于一致性（请看官了解一下所谓【一致性】，【base】，【acid】）的检查。
而同时，浏览数并不要求保证一致性，只要大概准确就行了。
所以这时候，我们可以先将浏览数写入 redis，满足一定条件后，再回写数据库。
比如，在 controller 中，让每次浏览都在 redis 上 +1，+1 完成后，检查浏览数是否除以 10 后余数为 0（&lt;code&gt;count % 10 === 0&lt;/code&gt;），是的话，则回写数据库，并将缓存置为 0。&lt;/p&gt;
&lt;h2 id="缓存过期策略"&gt;缓存过期策略&lt;/h2&gt;&lt;h3 id="可以通过过期时间来控制内容新鲜期"&gt;可以通过过期时间来控制内容新鲜期&lt;/h3&gt;
&lt;p&gt;那么就设置设缓存过期时间。比如在一个网站上，总会有一些每日之星用户，或者今日推荐文章。&lt;/p&gt;

&lt;p&gt;这些内容的新鲜期都很长，比如每日之星的数据，如果 20 分钟更新一次，用户也不会有异议。那么，我们在查询出这些用户后，可以将结果集存入缓存中，并设置过期时间为 20 分钟。待自动失效后，再重新查询。&lt;/p&gt;
&lt;h3 id="无法通过过期时间来控制内容新鲜期"&gt;无法通过过期时间来控制内容新鲜期&lt;/h3&gt;
&lt;p&gt;这时，又有两个策略了。一个是【主动过期】策略，一个是【被动过期】策略。比如想要缓存一篇文章的内容 HTMl，但文章的页面中包含了评论信息。一些老文章被大量访问而无人添加评论时，缓存的效果杠杠的。但一些近期文章会被用户添加评论
我们无法判断用户何时会添加评论，所以无法得到一个最佳实践的文章过期时间。&lt;/p&gt;
&lt;h4 id="主动过期"&gt;主动过期&lt;/h4&gt;
&lt;p&gt;顾名思义，主动地去 delete 缓存。还是上面的文章例子。我们可以在评论的 model 中，设置一个回调逻辑。每当评论被更新时，同时去删除评论所对应的文章的缓存内容。&lt;/p&gt;
&lt;h4 id="被动过期"&gt;被动过期&lt;/h4&gt;
&lt;p&gt;被动过期也不是完全不需要回调逻辑，只是相对主动过期来说。它不必理解缓存层的存在。&lt;/p&gt;

&lt;p&gt;还是上面的例子，当我们缓存一个文章页面时，不仅以文章的 id 为 cache key，还在 cache key 中拼入文章的 update_at 字段。
当评论更新时，让评论去 &lt;code&gt;touch&lt;/code&gt; 一下对应的文章，更新文章的最后修改日期。那么当用户再次访问文章时，由于 cache key 变动，过期的内容就不会被展现，从而实现了被动过期。&lt;/p&gt;

&lt;p&gt;同样的例子还有，一篇文章是以 markdown 写成，每次输出的时候，都要进行 markdown 渲染，这是个耗时操作。于是我们可以将 &lt;code&gt;'markdown_result_' + artical.id + artical.updated_at&lt;/code&gt; 作为 key，来缓存 markdown 的渲染结果。每当文章更新时，被动地废弃旧有的缓存结果。&lt;/p&gt;

&lt;p&gt;当然，这里不能说主动过期好，还是被动过期好。细心的看客也许在上面两个例子中发现了问题，那就是，当文章的内容没有进行改变，而评论添加时，文章却要重新渲染 markdown，可渲染结果其实是一样的。&lt;/p&gt;
&lt;h2 id="HTML 片段缓存"&gt;HTML 片段缓存&lt;/h2&gt;
&lt;p&gt;resource: &lt;a href="https://ruby-china.org/topics/21488" rel="nofollow" target="_blank"&gt;https://ruby-china.org/topics/21488&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="//dn-cnode.qbox.me/Fk-l-SeyP-Ga_B8pGNSVGCp4dmlL" title="" alt="QQ20150405-5.png"&gt;&lt;/p&gt;

&lt;p&gt;以 &lt;a href="https://cnodejs.org" rel="nofollow" target="_blank" title=""&gt;CNode&lt;/a&gt; 为例，我简单地划分了 1 2 3 4 四个部分。每个部分在逻辑上都是一个相对独立的 setion，它们使用不同的数据进行渲染。在代码组织上，这些部分也是属于不同的 view 文件来负责。&lt;/p&gt;

&lt;p&gt;4 的部分就是我们所说的，可以通过过期时间来管理的片段。这个部分 10 分钟更新一次没有问题。&lt;/p&gt;

&lt;p&gt;3 的部分类似上面 markdown 的例子，渲染是耗时的，而数据是经常不变的。所以我们可以通过类似 &lt;code&gt;'user_profile' + user.id + user.updated_at&lt;/code&gt; 的 cache key 来将其缓存。&lt;/p&gt;

&lt;p&gt;而 1 和 2 的部分，就类似上面【被动过期】的例子。1 中，不仅有帖子的标题，还有帖子的作者信息，还有帖子的最后回复者信息，粗略一算，这都是 3 条查询。如果能缓存起来，那是大大滴有用。而 2，包含了所有 1 类似的部分，也可以被缓存。但如果 1 动了，2 怎么办？所以在缓存 2 时，我们可以使用所有 1 中最新的那个帖子的更新时间来作为 key，当有帖子更新后，更新时间对不上，缓存就被动过期了。&lt;/p&gt;

&lt;p&gt;如果是个大型站点，1 的内容频繁动，那么会导致 2 的缓存命中率很低。这时，从业务上，我们判断，主页的新鲜期是可以在 5s 内不变的。这时，缓存策略可以改为，最新的帖子的更新时间，如果离现在的时间不超过 5s，则返回之前缓存的内容。我们一下就从【被动过期】的策略，变回【过期时间】的策略了。&lt;/p&gt;

&lt;p&gt;所以具体采用什么策略，根据业务场景可以灵活选择。&lt;/p&gt;

&lt;p&gt;【被动过期】策略时，切记要让上层片段的缓存 key 可以被下层 touch 更新。【过期时间】策略时，需要我们判断一下内容的新鲜期。&lt;/p&gt;

&lt;p&gt;并且有一点比较深入的知识点是，不同的 touch 策略，会对缓存命中率产生影响。这个知识点请参照本小节 resource 部分的链接去看看 Tower 在面对这个情况时的方案。&lt;/p&gt;

&lt;p&gt;如果你要问我 CNode 在片段缓存上是怎么选择的，我可以负责任并潇洒地告诉你：目前没有这方面的缓存~~~~&lt;/p&gt;

&lt;p&gt;说起来啊，一是访问量比较小，懒得做。二是，从技术上说，渲染是同步的，而在 Node.js 中，数据查询是异步的。我思考了一下，做这个片段缓存不是简单的事情。而 Rails 中做起来就简单多了，虽然玩 Node 的人总是觉得 Node 可以原生异步并发取数据是一件优越的事情。但同步 io 模型在这个地方带来的好处就是【惰性求值】
。Rails 在渲染时，可以判断一下到底是【查询 + 渲染】还是【直接取缓存】。而 Node 由于异步查询和同步渲染之间的冲突，要解决这个问题，必须有个方便地支持异步渲染的模板方案出现。&lt;/p&gt;
&lt;h2 id="last_modified 和 etag"&gt;last_modified 和 etag&lt;/h2&gt;
&lt;p&gt;resource: &lt;a href="http://robbinfan.com/blog/13/http-cache-implement" rel="nofollow" target="_blank"&gt;http://robbinfan.com/blog/13/http-cache-implement&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这节我们讨论的是静态页面在浏览器中的缓存思路。所以不是 max-age 和 cache-control 那套针对静态资源的方案，而是 last_modified 和 etag 这一套。&lt;/p&gt;

&lt;p&gt;上面的内容，一直在说数据库，缓存数据库。但有一点不可忽视的是，浏览器中其实也缓存了我们页面的副本，这部分的缓存，也应该有效地利用起来。
最简单利用方式，就是让服务器判断一下最终页面生成的 etag 与浏览器 header 中传来的 etag 是否相同的，相同的话，则返回 304，省去网络传输的带宽开销。&lt;/p&gt;

&lt;p&gt;注意，最简单的方式是判断最终内容生成的 etag！其实我们可以自定义 etag。在这里，etag 也可以理解成一定意义上上述的 cache key，只是这回，储存介质变成了用户的浏览器。&lt;/p&gt;

&lt;p&gt;还是上面那个文章内容页面的例子，我们文章页面由 文章内容 + 评论 内容决定是否缓存。这时，我们可以把文章内容的更新时间和最新评论的更新时间拼成一个 etag，返回给用户。下次用户再访问时，如果 etag 对得上，服务端根本都不需要再去缓存数据库中取 HTML 片段数据，直接告诉用户一个 304，【内容与上次一样，没变化】。这时浏览器就直接从自己的缓存中取出页面进行展示了。既节省了宽带占用，又节省了查询开销。&lt;/p&gt;
&lt;h3 id="etag as cookie"&gt;etag as cookie&lt;/h3&gt;
&lt;p&gt;这里说点题外话，etag 在一定意义上是可以拿来当 cookie 用的。首先我们要了解，浏览器针对每一个 url（包括 querystring 部分）都可以存储一个 etag 值。&lt;/p&gt;

&lt;p&gt;比如我是一个广告服务商，我的广告页面是 &lt;a href="https://cnodejs.org/ads" rel="nofollow" target="_blank"&gt;https://cnodejs.org/ads&lt;/a&gt;。每当不同的用户访问这个页面时，我都根据大数据黑魔法定位到这个匿名用户到底是谁，然后返回他感兴趣的内容。可如果用户禁用了 cookie 的话，我该怎么定位用户呢？这时候可以使用 etag。每当用户不带 etag 访问时，都生成一个不冲突的 etag 给它，那么下次他再访问我 url 时，etag 就回来了。&lt;/p&gt;

&lt;p&gt;OK，结束了，结尾语是：Rails 社区代表 Web 开发世界的最先进生产力。&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Sun, 05 Apr 2015 18:27:59 +0800</pubDate>
      <link>https://ruby-china.org/topics/25009</link>
      <guid>https://ruby-china.org/topics/25009</guid>
    </item>
    <item>
      <title>一个面向 Node.js 初学者的系列课程：node-lessons</title>
      <description>&lt;p&gt;GitHub repo 地址：&lt;a href="https://github.com/alsotang/node-lessons" rel="nofollow" target="_blank"&gt;https://github.com/alsotang/node-lessons&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如果大家认为漏了哪些初学者应会的内容，可以在此留言，或者开个 issue 给我（!! 推荐）。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="《Node.js 包教不包会》 -- by alsotang"&gt;《Node.js 包教不包会》 -- by alsotang&lt;/h2&gt;&lt;h2 id="为何写作此课程"&gt;为何写作此课程&lt;/h2&gt;
&lt;p&gt;在 CNode(&lt;a href="https://cnodejs.org/" rel="nofollow" target="_blank"&gt;https://cnodejs.org/&lt;/a&gt;) 混了那么久，解答了不少 Node.js 初学者们的问题。回头想想，那些问题所需要的思路都不难，但大部分人由于练手机会少，所以在遇到问题的时候很无措。国内唯一一本排的上号的 Node.js 书是 @朴灵 (&lt;a href="https://github.com/JacksonTian" rel="nofollow" target="_blank"&gt;https://github.com/JacksonTian&lt;/a&gt;) 的《深入浅出 Node.js》(&lt;a href="http://book.douban.com/subject/25768396/" rel="nofollow" target="_blank"&gt;http://book.douban.com/subject/25768396/&lt;/a&gt; )，但这本书离实战还是比较远的。&lt;/p&gt;

&lt;p&gt;这个课程是希望提供更多的 Node.js 实战机会，通过每一节精心安排的课程目标，让 Node.js 的初学者们可以循序渐进地，有目的有挑战地开展 Node.js 的学习。&lt;/p&gt;

&lt;p&gt;更多 Node.js 入门资料请前往：&lt;a href="https://cnodejs.org/getstart" rel="nofollow" target="_blank"&gt;https://cnodejs.org/getstart&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="课程列表"&gt;课程列表&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Lesson 0: &lt;a href="https://github.com/alsotang/node-lessons/tree/master/lesson0" rel="nofollow" target="_blank" title=""&gt;《搭建 Node.js 开发环境》&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lesson 1: &lt;a href="https://github.com/alsotang/node-lessons/tree/master/lesson1" rel="nofollow" target="_blank" title=""&gt;《一个最简单的 express 应用》&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lesson 2: &lt;a href="https://github.com/alsotang/node-lessons/tree/master/lesson2" rel="nofollow" target="_blank" title=""&gt;《学习使用外部模块》&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lesson 3: &lt;a href="https://github.com/alsotang/node-lessons/tree/master/lesson3" rel="nofollow" target="_blank" title=""&gt;《使用 superagent 与 cheerio 完成简单爬虫》&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lesson 4: &lt;a href="https://github.com/alsotang/node-lessons/tree/master/lesson4" rel="nofollow" target="_blank" title=""&gt;《使用 eventproxy 控制并发》&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lesson 5: &lt;a href="https://github.com/alsotang/node-lessons/tree/master/lesson5" rel="nofollow" target="_blank" title=""&gt;《使用 async 控制并发》&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lesson 6: &lt;a href="https://github.com/alsotang/node-lessons/tree/master/lesson6" rel="nofollow" target="_blank" title=""&gt;《测试用例：mocha，should，istanbul》&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lesson 7: 《测试用例：supertest》&lt;/li&gt;
&lt;li&gt;Lesson 8: 《Mongodb 与 Mongoose 的使用》&lt;/li&gt;
&lt;li&gt;Lesson 9: 《一个简单的 blog》&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="License"&gt;License&lt;/h2&gt;
&lt;p&gt;MIT&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Tue, 07 Oct 2014 21:50:32 +0800</pubDate>
      <link>https://ruby-china.org/topics/21904</link>
      <guid>https://ruby-china.org/topics/21904</guid>
    </item>
    <item>
      <title>ruby-china 有 turbolinks 的 issue</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2014/90629142af3ff56763200db636282b1e.png" title="" alt=""&gt;
切换页面时，每次都新增一个 ga 的 script 链接。&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Thu, 25 Sep 2014 01:05:41 +0800</pubDate>
      <link>https://ruby-china.org/topics/21715</link>
      <guid>https://ruby-china.org/topics/21715</guid>
    </item>
    <item>
      <title>Ruby 为什么要抄袭 Python？</title>
      <description>&lt;p&gt;如图
&lt;img src="https://l.ruby-china.com/photo/2014/67dff06d03bcf2ebef2c0c100bfe5da4.png" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Mon, 15 Sep 2014 12:22:40 +0800</pubDate>
      <link>https://ruby-china.org/topics/21531</link>
      <guid>https://ruby-china.org/topics/21531</guid>
    </item>
    <item>
      <title>git merge 的坑</title>
      <description>&lt;p&gt;时不时就听说 git merge 的时候会有坑，比如两行 goto fail 之类的。&lt;/p&gt;

&lt;p&gt;可是自己没有遇到过。&lt;/p&gt;

&lt;p&gt;google 了一下，好像也不是那么容易找到。&lt;/p&gt;

&lt;p&gt;大家可以说说自己遇到的坑吗？&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Wed, 05 Mar 2014 11:57:43 +0800</pubDate>
      <link>https://ruby-china.org/topics/17674</link>
      <guid>https://ruby-china.org/topics/17674</guid>
    </item>
    <item>
      <title>[询问] 不使用 Rails 开发 Web 程序时，需要注意的地方</title>
      <description>&lt;p&gt;之前有接触过 Rails，知道 Rails 帮助程序员做了很多的事情。最近打算用 Node.js 的 Express 框架以及前端的 Ember.js 来开发程序。&lt;/p&gt;

&lt;p&gt;想问问各位大大，我以下的几点注意事项还有没有需要添加的地方？&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;后端：&lt;/p&gt;

&lt;p&gt;程序正确性：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;尽可能覆盖率高的测试代码&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;安全：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;csrf 标签&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;注意对用户的输入进行 escape，防止注入。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;前端：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JS、CSS 文件的 minify 与打包成单文件&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>alsotang</author>
      <pubDate>Sun, 30 Jun 2013 18:48:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/12120</link>
      <guid>https://ruby-china.org/topics/12120</guid>
    </item>
    <item>
      <title>[Cyberspace] Enumerable 小组招募 90 后 Web 开发爱好者</title>
      <description>&lt;p&gt;大家好！我是 Enumerable 小组的联合发起人 &lt;a href="/alsotang" class="user-mention" title="@alsotang"&gt;&lt;i&gt;@&lt;/i&gt;alsotang&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;之所以想创建这么个团队，与这个帖子有一定的关系： 《技术大牛@曹政（caoz）曾经有一个神秘的 QQ 群》 &lt;a href="http://www.zhihu.com/question/20807920" rel="nofollow" target="_blank"&gt;http://www.zhihu.com/question/20807920&lt;/a&gt; 。&lt;/p&gt;

&lt;p&gt;我相信，&lt;a href="/caoz" class="user-mention" title="@caoz"&gt;&lt;i&gt;@&lt;/i&gt;caoz&lt;/a&gt; 当年在创建这个 QQ 群的时候，一定也还是一个默默无闻的小子，他只是凭着自己对于互联网的热情，找了一些同样对互联网有热情的小子一起，创建了这样一个 QQ 群。之后，这个 QQ 群里的石头不断相互碰撞打磨，最终一个个都变得美丽光滑。（磨石头的比喻引自《乔布斯：遗失的访谈》： &lt;a href="http://www.huxiu.com/article/14067/1.html" rel="nofollow" target="_blank"&gt;http://www.huxiu.com/article/14067/1.html&lt;/a&gt; ）&lt;/p&gt;

&lt;p&gt;很多朋友或许想象过像当年的&lt;a href="/caoz" class="user-mention" title="@caoz"&gt;&lt;i&gt;@&lt;/i&gt;caoz&lt;/a&gt; 一样，找到一批志同道合的朋友，与他们一起在互联网世界中探索和玩乐，但却没有付诸实践。而这篇招募贴，正是对这一想法的实践。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;我先来介绍一下自己，以及另外一位联合发起人 &lt;a href="/hit9" class="user-mention" title="@hit9"&gt;&lt;i&gt;@&lt;/i&gt;hit9&lt;/a&gt; 。&lt;/p&gt;

&lt;p&gt;&lt;a href="/alsotang" class="user-mention" title="@alsotang"&gt;&lt;i&gt;@&lt;/i&gt;alsotang&lt;/a&gt; 的介绍在此： &lt;a href="http://fxck.it/about" rel="nofollow" target="_blank"&gt;http://fxck.it/about&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="/hit9" class="user-mention" title="@hit9"&gt;&lt;i&gt;@&lt;/i&gt;hit9&lt;/a&gt; 的介绍在此： &lt;a href="http://hit9.org/about.html" rel="nofollow" target="_blank"&gt;http://hit9.org/about.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="/hit9" class="user-mention" title="@hit9"&gt;&lt;i&gt;@&lt;/i&gt;hit9&lt;/a&gt; 在哈工大，我在川大，我们从未见过面。如果你加入了我们，那或许大家也没机会见面。&lt;/p&gt;

&lt;p&gt;如果在用心看了我们的博客和 GitHub 后，你觉得自己与我们对于互联网的热情是一样的，那么你应该就是我们要寻找的人。：）&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;招募要求：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;必须是 90 后。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;高于同龄人的 Web 开发编码水平，并喜欢使用 Unix-like 操作系统。（通过 GitHub 来证明）&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;喜爱表达与分享。（通过博客来证明）&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;愿意奉献时间参与到开源项目的开发中。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;工作职责：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;积极发起并主导一个在 GitHub 上开源的有意思的项目。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;积极参与小组其他成员在 GitHub 上开源的有意思的项目。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;待遇：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;薪水：0&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;户口：不帮解决&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;免费的饮料和水果：无&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;工作方式：SOHO&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mac：不配备&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;地点：Cyberspace&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;届时，你会被加入到一个 Google Groups 的邮件列表中来，我们通过邮件、Dropbox 和 GitHub 来进行协作。这个邮件列表的上线人数是 10 人，因为我们认为一个小圈子里面的人会比较互相了解和熟悉，沟通成本低。由于圈子较小，大家也会更有责任感地参与到圈中人发起的开源项目活动中。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;联系我们：&lt;/p&gt;

&lt;p&gt;我的邮箱是 alsotang@gmail.com。有意愿的朋友，请发送你的 GitHub 地址与博客地址过来，无需简历。：）&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;FAQ：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;为什么团队叫 Enumerable？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Enumerable 是 Ruby 中的一个核心 module。如果一个自定义类实现了 :each 这个实例方法，那么就可以通过 include 这个 module，来获得诸如 [:map, :reduce, :select, :sort] 之类的许许多多迭代方法。&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Sat, 18 May 2013 14:38:31 +0800</pubDate>
      <link>https://ruby-china.org/topics/11078</link>
      <guid>https://ruby-china.org/topics/11078</guid>
    </item>
    <item>
      <title>《Ruby 元编程》中的禅师</title>
      <description>&lt;p&gt;&lt;em&gt;声明：我不知道这样的摘录算不算侵犯版权哈～如果侵犯的话，麻烦留言告诉我，我删帖。&lt;/em&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;一位编程大师在高山之巅沉思着。他想得如此投入，代码和他的灵魂交织得如此紧密，以至于他开始发出轻微的鼾声。&lt;/p&gt;

&lt;p&gt;一个门徒爬上了山，打断了大师的冥想。『我很困惑，大师，』门徒说道。『我学了很多高级的技术，但是我还是不知道该怎样正确地运用它們。告诉我，元编程的精髓是什么？』&lt;/p&gt;

&lt;p&gt;『看看我旁边的这棵小树吧，』大师回答道，轻轻地挥动着手臂，『看看它是怎样精巧地弯向地面的，仿佛要回馈自己的根一样。这就是编程要达到的境界：简单而直白，并且最终会回归自身，想一个循环一样。』&lt;/p&gt;

&lt;p&gt;『我还是不明白，大师，』门徒更加困扰了，『我听人說可以自我修改的代码不好，如何知道我恰当地使用了这门技艺呢？』&lt;/p&gt;

&lt;p&gt;『用纯净的心来看待自己的代码，』大师教导门徒道。『这样就会清楚何时自己的代码开始变得晦涩难懂。用自己的知识使之变得清晰，而不是晦涩和困惑。』&lt;/p&gt;

&lt;p&gt;『但是大师，』门徒争辩道，『我缺乏经验，需要简单的规则来辨别是非。』&lt;/p&gt;

&lt;p&gt;大师烦了。『虽然你够聪明，小子，』大师说道，『但是你有足够的智慧去忘掉所学的东西么？ &lt;strong&gt;根本没有什么元编程，只有编程而已&lt;/strong&gt; 。走吧让我继续平静地沉思。』&lt;/p&gt;

&lt;p&gt;听了这些话，门徒顿悟了。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;好高深的禅道.....&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Tue, 07 May 2013 19:45:44 +0800</pubDate>
      <link>https://ruby-china.org/topics/10793</link>
      <guid>https://ruby-china.org/topics/10793</guid>
    </item>
    <item>
      <title>Rails 安全性小探</title>
      <description>&lt;p&gt;原文在：&lt;a href="http://fxck.it/post/49101760571/rails" rel="nofollow" target="_blank" title=""&gt;http://fxck.it/post/49101760571/rails&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;背景：用户的电脑并未被植入任何木马，但当出了用户电脑的网卡后，面临着一个不安全的网络世界。下面提到的某些技术的基础知识就不涉及了，只涉及跟 Rails 相关的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="玩嗅探："&gt;玩嗅探：&lt;/h2&gt;
&lt;p&gt;如果你是通过 HTTP 来访问一个已经登陆了的网站，那么只能说，这种方式的访问的安全性是永远无法保障的。随便一个人截取了你的 cookie 就可以冒充你登陆该网站。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="玩逻辑："&gt;玩逻辑：&lt;/h2&gt;
&lt;p&gt;CookieStore。&lt;/p&gt;

&lt;p&gt;今天看资料的时候看到 ActionDispatch::Session::CookieStore 这个东西，它会把 session 存储中的相关信息存在用户的浏览器端然后加个指纹上去。而与加密有关的 secret 保存在 config/initializers/secret_token.rb 这个文件中。&lt;/p&gt;

&lt;p&gt;这么说来，这个 secret_token.rb 肯定就不能给其他人看到咯，否则就存在被攻击者伪造任意 session 的可能。在部署开源的 Rails 相关项目时，除了 database.yml 这个文件需要隐藏外，有必要时，也需要隐藏 secret_token.rb 这个文件。&lt;/p&gt;

&lt;p&gt;Rails 出箱（out of box）即附带了 session 的功能，所以不用进行任何设置就可以对针对某一用户的 session 进行操作。识别用户是通过 session_id 来的，session_id 可以认为是不可破解或猜测的，只能嗅探得到。&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;我还原了一下 ruby-china 在我浏览器上设置的 session（抹掉了信息，只保留了格式）&lt;/p&gt;

&lt;p&gt;{"session_id"=&amp;gt;"06fd9f57exxxxxxxxxxxxxxxxxxxx0dd",
     "_csrf_token"=&amp;gt;"eddCzjiFBIeGrrxxWxxxxxxxxxxxxxxxxxxxxps+Hxc=", 
    "warden.user.user.key"=&amp;gt;["User", [1848],
    "$2a$10xxxxxxxxxxxxxxxKDX.YIpO"]}
首先 session_id 这关的伪造肯定过不了。&lt;/p&gt;

&lt;p&gt;_csrf_token 貌似在我们的话题中不是很必要。&lt;/p&gt;

&lt;p&gt;warden.user.user.key 虽然表露出：我在 User 表中，且 id 是 1848（我是 ruby-china 的 1848 号会员），但是 warden 在后面加了个 digest，所以也无法伪造。这个 digest 应该是存在于 User 表中的某个字段里。&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;很多时候，如果我们并没有 devise 或者 warden 这样的库的时候，我们会简单地保存 user_id 这个值在 session 中（用的时候，会在服务端取出 session[:user_id] 来得到 current_user）。在这样的情况下，如果不小心暴露了自己的 config/initializers/secret_token.rb 文件，那么，攻击者就可以简单地把 user_id 修改为其他用户的，并通过适当的算法得到 CookieStore 生成的那个 digest，然后 do evil。&lt;/p&gt;

&lt;p&gt;这点需要好好注意。&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;以上提到的『适当的算法』在此：&lt;a href="https://github.com/joernchen/evil_stuff/blob/master/ruby/sign-cookie.rb" rel="nofollow" target="_blank"&gt;https://github.com/joernchen/evil_stuff/blob/master/ruby/sign-cookie.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ruby-china 的 secret_token.rb 在此：&lt;a href="https://github.com/ruby-china/ruby-china/blob/master/config/initializers/secret_token.rb" rel="nofollow" target="_blank"&gt;https://github.com/ruby-china/ruby-china/blob/master/config/initializers/secret_token.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;这么说来，独立开发者的 Web 开发学习成本真的很高啊....不仅前端后端，还要注意各种安全性问题。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="玩 CSRF："&gt;玩 CSRF：&lt;/h2&gt;
&lt;p&gt;在 Rails 中，默认的 CSRF 机制貌似已经挡掉这个攻击方式了。&lt;/p&gt;

&lt;p&gt;CSRF 虽然是个危害不小的技术，但是当网站在应用了某个 CSRF 的中间件时（Rails provide it out of box），就已经能成功阻挡这类攻击了。除非....&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="玩 XSS："&gt;玩 XSS：&lt;/h2&gt;
&lt;p&gt;...除非玩 XSS。其实 CSRF 跟 XSS 这两类攻击还是分得蛮开的，记得 cnodejs 有一次被大家玩 XSS 玩疯了，我当时也中了招。如果你的网站可以被人插入 JS 代码进行 XSS 攻击的话，HTTPS 和 CSRF 之类的东西就都无法保住用户的安全了，用户只能被随意宰割。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="防护："&gt;防护：&lt;/h2&gt;
&lt;p&gt;关于 Rails 的安全性，这篇 guides 还挺好看的：&lt;/p&gt;

&lt;p&gt;&lt;a href="http://guides.rubyonrails.org/security.html" rel="nofollow" target="_blank" title=""&gt;http://guides.rubyonrails.org/security.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;XSS 和 CSRF 的话，也推荐两篇我收藏的文章：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html" rel="nofollow" target="_blank" title=""&gt;http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://snoopyxdy.blog.163.com/blog/static/60117440201284103022779/" rel="nofollow" target="_blank" title=""&gt;http://snoopyxdy.blog.163.com/blog/static/60117440201284103022779/&lt;/a&gt; （正是此牛人带头玩起了 cnodejs 的 XSS 游戏）&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>alsotang</author>
      <pubDate>Mon, 29 Apr 2013 00:51:46 +0800</pubDate>
      <link>https://ruby-china.org/topics/10603</link>
      <guid>https://ruby-china.org/topics/10603</guid>
    </item>
    <item>
      <title>如果做一个基于的 Rails 开源项目，哪些项目文件属于隐私？</title>
      <description>&lt;p&gt;今天看资料的时候看到 ActionDispatch::Session::CookieStore 这个东西，它会把 session 的相关信息存在用户的浏览器端然后加个指纹上去。而与加密有关的 secret 保存在 environment.rb 这个文件中。&lt;/p&gt;

&lt;p&gt;这么说来，这个 environment.rb 肯定就不能给其他人看到咯，否则就会被攻击者伪造任意 session。&lt;/p&gt;

&lt;p&gt;那么我想知道，除了这个 environment.rb 之外，还有哪些文件是属于项目的隐私文件呢？如果项目中用了 CookieStore，devise，mysql，redis 的话。（或更多）&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Sun, 28 Apr 2013 22:09:58 +0800</pubDate>
      <link>https://ruby-china.org/topics/10599</link>
      <guid>https://ruby-china.org/topics/10599</guid>
    </item>
    <item>
      <title>四川大学软件学院大三学生求 Web 开发相关的工作 (已结帖)</title>
      <description>&lt;p&gt;简历贴过来可能容易格式乱...所以只好贴地址了：
&lt;a href="https://docs.google.com/document/d/1nHPBgZknksOzyVTSN4l-PdAeOyFVv6DgYofyaJGzflE/edit?usp=sharing" rel="nofollow" target="_blank"&gt;https://docs.google.com/document/d/1nHPBgZknksOzyVTSN4l-PdAeOyFVv6DgYofyaJGzflE/edit?usp=sharing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;麻烦有招聘需求的大神们可以腾出两三分钟看看。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;最后是去了淘宝的一个数据部门，跟@朴灵，@苏千 他们一起工作。&lt;/p&gt;

&lt;p&gt;在此请容我谢谢各位社区朋友之前的支持！&lt;/p&gt;</description>
      <author>alsotang</author>
      <pubDate>Mon, 22 Apr 2013 17:34:11 +0800</pubDate>
      <link>https://ruby-china.org/topics/10406</link>
      <guid>https://ruby-china.org/topics/10406</guid>
    </item>
  </channel>
</rss>
