总结项目遇到的几个问题

  • February 22, 2022 13:11
  • Posted by jane
  • 0 comments

1、线上用户串号

首先说一个印象比深刻的用户串号的问题。

情况是这样的,当时用户访问网页时,不管是之前登录过的还是没登录过的,都自动登录到同个用户上面去了。检查之后,发现跟最近做的调整有关:用户自动延长登录时长,给频繁调用的接口添加缓存。简单说明一下,自动延长登录时长是通过在接口返回头中带上新的登录 token,用户端检测到新 token 时就替换掉旧的,这样能避免用户每过段时间就重新登录一次。

问题就出在这里,接口使用 Garner 插件做缓存处理时,一不注意把 headers 中的 token 字段也缓存了进去,导致所有用户访问时都获取到缓存的 token ,登录到该 token 对应的用户。

如下配置可以看出 Refresh-Access-Token header 被加入了缓存(具体配置参考:Add support for caching both body and headers in Garner::Mixins::Rack

module Garner
  module Cache
    class Identity
      TRACKED_HEADERS = %w[Refresh-Access-Token X-Total-Count X-Total X-Total-Pages X-Per-Page X-Page X-Next-Page X-Prev-Page X-Offset]
      alias_method :_fetch, :fetch
      def fetch(&block)
        if @key_hash[:tracked_grape_headers]
          result, headers = _fetch do
            result = yield
            headers = @ruby_context.header.slice(*TRACKED_HEADERS) if @ruby_context.respond_to?(:header)
            [result, headers]
          end
          ...
      end
    end
  end
end

解决:把 token header 的缓存去掉,修改 token 生成秘钥,使旧 token 无效,促使所有用户重新登录自己的账号。

2、Rails 更新 nio4r 插件后需重启 puma

rails 部署后报错:

/home/deploy/.rvm/gems/ruby-2.6.5/gems/bundler-2.2.13/lib/bundler/runtime.rb:302:in `check_for_activated_spec!': You have already activated nio4r 2.5.7, but your Gemfile requires nio4r 2.5.8. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)

可以看出是因为 nio4r 插件更新后,服务器还在使用旧版的 nio4r,导致 Puma 挂掉。我们可以通过重启服务来解决问题,注意需分两步执行:

cap production puma:stop
cap profuction puma:start

3、Rails 并发更新时应避免用到旧缓存数据

问题描述:如下代码,假设有两个线程A、B同时请求用户锁,线程B先获得锁资源,并更新了该用户的 remaning_amount。那么在线程A获得锁资源后,由于Rails的SQL缓存机制,线程A计算用户剩余金额时会用到旧的缓存数据,导致更新结果不正确。

# 用户使用钱包抵扣后,减少剩余金额
user.with_lock do
  user.update(remaining_amount: user.remaining_amount - 100)
end

解决:可以使用 reloadupdate_counters 来避免用到缓存数据。

# 1、 reload 会重新从数据库获取用户数据
user.update(remaning_amount: user.reload.remaining_amount - 100)

# 2、update_counters 是原操作,从 sql 语句中可以看出它是直接读 db 数据来更新的
User.update_counters(user.id, remaining_amount: -100)
# User Update All (16.4ms)  UPDATE "users" SET "remaining_amount" = COALESCE("remaining_amount", 0) - $1 WHERE "users"."id" = $2  [["remaining_amount", 100], ["id", 1]]

4、ActiveRecord::Deadlocked 死锁

假设有两个并行的事务,它们的信息如下所示。两事务并发执行,几乎在同时刻,事务1更新了商品A,事务2也更新了商品B。接下来当事务1打算更新商品B时,发现商品B被事务2占用了,所以等待事务2 完成 commit 操作。事务2打算更新商品A,却被事务1占用了,所以也在等待事务1结束,这样就出现了死锁。

事务1:
  订单1:
    订单项1:商品A
    订单项2:商品B
事务2:
  订单2:
    订单项1:商品B
    订单项2:商品A

总的来说,这是由于对公共资源按照不同顺序进行更新导致的。

解决:商品只需要统一按照顺序或倒叙排序处理就可以避免死锁了

5、i18n 配置不当出现的奇怪现象

问题描述:网页把语言设置成非英文的,但是在电脑或手机系统语言设置成英文后 ,还是会显示出英文翻译。
使用框架、插件:Vue, vue-i18n
原因:有些地方只有英文翻译,其他语言留空,所以在代码中只定义了英文的翻译。比如下面这个例子就只有定义英文翻译的 hourminute

<i18n>
{
  "zh_cn": {
    "started": "距开始"
  },
  "zh_hk": {
    "started": "距開始"
  },
  "en": {
    "started": "start:",
    "hour": "h",
    "minute": "min left"
  },
  "ja": {
    "started": "開始まで"
  }
}
</i18n>

再看下 i18n 配置,fallbackLocale 被设置成浏览器的语言了。如果有翻译缺失,会默认使用 fallbackLocale 设置的语言。所以系统语言设置成英文后,即使网页切换成了非英文语言,也还会显示英文翻译的 hourminute

export const i18n = new VueI18n({
  fallbackLocale: _.snakeCase(navigator.language),
  ...
});

解决:其他语言翻译留空时也定义下变量,内容设置为空字符串。

6、IOS 添加书签、保存网页到手机,图标显示不正确

问题描述:网页有设置 favicon 图标,但是 IOS 端在 添加网页到书签/桌面时,图标没有显示设置的 favivon。
解决:HTML header 中需设置不同尺寸的 apple-touch-icon link 标签。参考文档:IOS default touch icon sizes

Comments