前段时间给程序加了个功能,打印时间按照时区打印,具体看起来是这样

    loc, _ := time.LoadLocation("Asia/Chongqing")
    return time.Now().In(loc).Format("2006-01-02 15:04:05")

在本地运行没有丝毫问题,于是就想当然的推到了线上,结果线上的环境在进行到这步的时候直接panic了,嗯,看来是过于的自信导致了问题,明显loc是nil了,那么打印下错误吧,结果看到了这么一行:

open /usr/local/go/lib/time/zoneinfo.zip: no such file or directory

于是瞬间就明白了,我们线上使用的是docker镜像,复制到容器里的go程序也是编译好的二进制文件,所以容器里没有go环境,导致在本地能正常运行的程序在线上因为缺少对应文件而panic。

知道了问题,就好解决了,当然这里可以直接换加了go环境的镜像,但是明显这样违背了docker作为线上生产环境的初衷,即最小可用,加入了go环境会直接把大量的冗余数据加入到容器中,使单个容器的体积直接倍增,所以我们只需要把这个特定依赖的文件复制到容器中即可,在Dockerfile中加入这样一段:

    COPY ./zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip
    // 这里我把这个文件也在本地复制到了我的项目中,因为我们的打包上线工作由线上的CI服务来完成,如果是在本地打包,那么可以直接复制原文件
    COPY /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip

最后说一句,下划线无视error什么的,是真不可取啊,吸取教训把上面的代码也换成了

     loc, err := time.LoadLocation("Asia/Chongqing")

	if err != nil {
		LogError("parse location err:", err)
		loc = time.Local
	}

	return time.Now().In(loc).Format("2006-01-02 15:04:05")

升级了新的版本之后,一切介正常,唯独发现pma打不开了,变成了一堆php代码,猜想是自带的Apache出了问题,参考网上其他教程,打开了httpd.conf,发现果然和php有关的那个loadmodule被注释掉了,打开,重启Apache,无果。继续Google,发现还要加上AddType和AddHandler,加上,重启Apache,仍然无果,但发现php info检测的代码可以正常打开了,遂怀疑是自带的PHP升级了,不再兼容当前版本的pma了,于是按照官方流程升级pma,重新打开,正常工作,解决战斗。

以下是详细三部曲:

  • 修改httpd.conf, 开启PHP loadmodule

打开Terminal,输入:sudo nano /etc/apache2/httpd.conf, 对httpd.conf进行编辑,按ctrl+W,搜索php7_module,找到:#LoadModule php7_module libexec/apache2/libphp7.so,将此行前的#号去掉,ctrl+O保存(此处可能弹出finder,关掉即可),ctrl+X退出

参考文献: PHP精通 mac本地安装php环境 找不到webserver 更改php文件目录 运行

  •  修改httpd.conf,增加php文件支持

同上,搜索IfModule,在任意位置添加如下代码:

<IfModule php7_module>
AddType application/x-httpd-php .php
AddType application/x-httpd-php-source .phps

<IfModule dir_module>
DirectoryIndex index.html index.php
</IfModule>
</IfModule>

参考文献:PHPMyAdmin showing code instead of webpage

  • 升级到最新版本的pma

    1. 从以下网址下载最新版本的PHPmyadmin:https://www.phpmyadmin.net/downloads/
    2. 重命名旧的pma文件夹(例如:phpmyadmin-old)
    3. 解压并复制并重命名刚刚下载好的新版本pma到需要的位置(例如:phpmyadmin)
    4. 从旧的文件夹中拷贝config.inc.php文件到新文件夹中
    5. 测试是否一切正常工作
    6. 删除旧文件夹

翻译自官方文档:phpMyAdmin-Docs-安装-从旧版本更新

PS:Reference websites may require ladders.

最近在学习docker和kubernetes,无奈本机搭了一个minikube无论如何无法启容器,k8s的日志就只告诉说没起来我在重启,剩下什么原因也不说,于是决定先在本地docker启动一下看看。

原本的后端应用上线是我写好之后由我的boss统一上线的,所以Dockerfile也不是我写的,当然线上能跑,本地就肯定能跑啦,于是直接docker build,docker run,然后就出现问题了

./main: line 1: syntax error: unexpected “(”

随便搜了一下,发现基本上出现这个问题是因为运行程序的内核不同,然后仔细研究了一下Dockerfile,发现使用的是alpine的镜像,这个镜像的特点就是非常小,只有几兆,而且对比了一下公司的Dockerfile和golang官方alpine镜像的Dockerfile,发现我的Dockerfile里并没有集成go语言环境,而是直接将编译好的程序拷贝到镜像中,那么问题其实就可以确定了,我本地编译的基于Mac的go程序,即基于darwin内核的go程序,而alpine是基于Linux的内核的,自然无法运行。

知道了问题解决方法就很好办了,如果使用liteIDE的同学更是省事,直接删掉原本编译好的程序,选择cross-linux64环境编译就好(不用去改现有的darwin环境)

就算不是使用的LiteIDE,那也可以使用如下语句打包:

cd $GOPATH
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ./make.bash

以上两步是为了生成linux平台所需要的工具和环境,只需要运行一次即可,如果使用liteIDE的话在切换环境的同时就会自动帮你准备好这些的
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build

再次运行docker,成功启动容器!

最近开始做一个通用抽奖的项目,项目中有一个活动用户每天的抽奖限制和总中奖限制,即每天最多抽多少回和活动期间总共中多少个。这一判断的特定就是请求量巨大,肯定是不能直接依靠关系数据库来做判断的,所以就要用到了缓存,将每一位用户的这两个限制都写到缓存中,需要判断限制的时候,直接读缓存就好。
首先先否定内存缓存,因为尽管缓存的数据是非常小的,但是参与抽奖的用户数量会很大,更何况一个应用会服务多个活动,那么用户数量更加不可估计,内存缓存很容易挤爆并丢失数据。
当然还有一个原因就是环境,项目的环境是容器,多容器之前无法共享内存缓存中的数据,而抽奖限制要求又必须是唯一的,所以内存缓存只能抛弃。
于是就来到了redis。
最开始的缓存逻辑,是将用户的每日抽奖限制和总中奖限制缓存在一起,在需要更新时,是将限制人为的+1,然后更新缓存。这几乎是新接触后端开发的新手都会犯的错误,这样的项目上线是一定会超发奖品的,因为后端应用是多线程并行运转。就比如限制中奖5个,一个用户已经中了3次,这时他找了个脚本,同时发了100次请求,那么这100次请求全部会读这一个限制,然后全部通过限制判断,100个都可能中奖了。会发生这种情况的原因就是没有应用到锁机制。
熟悉MySQL的同学应该都清楚MySQL的锁和事务机制,这里就不详细说了。如果redis像MySQL一样支持锁机制和事务,那么我们的问题不就迎刃而解了?
这样想的我真的是naive了,redis即支持锁机制又有事务,但就恰恰并没有解决我的问题。(当然这里的锁机制并非其他应用使用redis实现分布式锁的概念)。
redis的锁本质上是一种CAS(check-and-set)操作,即乐观锁,使用watch命令配合muti-exec的事务操作来实现,即在watch key1,之后在执行事务之前会去检验key1中的值是否有被更改过,如果有被更改过,则拒绝执行事务。乍一看这是能解决我的问题的,你在每个用户进来抽奖的时候watch这个用户的对应限制key不就可以了?
但很遗憾并非如此
首先,redis的muti-exec事务操作并非像MySQL中的事务一样为原子操作,一系列操作中的某一个操作出现问题了,redis会记录此次问题然后继续执行余下的操作,而且不具备rollback的功能。
其次,redis的事务对多线程的操作是不安全的(尽管redis服务端是单线程的),exec操作会清空watch key的列表,即假如先后进来抽奖者a和b,分别watch了key1和key2,a先判断完了执行了exec操作,redis会把key1和key2都清除,这种时候b的操作的安全性就无法保障了。
当然这并非不可解,用于我上面提到的redis的server端是单线程的,那么其实redis除了事务操作的其他命令,本身就是原子的,那么我们就可以考虑将两个限制分开存储,然后直接使用redis的自增操作。redis的自增操作会返回自增之后的值,只要拿着这个值去判断一下,就知道是否存在并发超发了。
但是自增也不是完美的,因为我们不可能把所有用户的限制一直缓存起来,肯定是要有过期时间的,否则即使是redis也可能会被撑爆。基于项目的考量,过期时间设置为每天的0点,即用户第二天就可以再次抽取。而一旦设置了过期时间,那么就可能出现如下情况
情况1:
抽奖活动限制每人最多中奖5次,中奖率100%,这个限制存在于redis中,每天0点过期(redis2.6版之前key的过期延迟为1秒,2.6版本之后过期延迟为1毫秒),那么在key即将过期之前,有一个已经中了3次奖的用户发来了4次请求(不论以何种手段),每次请求均按照该用户已中奖3次通过了中奖限制验证,4个请求均去申请自增中奖限制,而4个请求在去申请中奖限制时时间均超过了该key的过期时间,redis的自增请求会判断为key不存在,并从0开始重新+1,每个自增的请求结果都是不大于抽奖限制的,则该用户最后的限制变为了4,而其实他已经中走了7个奖。
通过incr操作返回自增后的值的特性,可以解决如上问题,即判断自增之后的值是否大于当前已知的限制值,如果不大于,那么说明本次自增是在错误的基础上自增的,应该认定为此次操作用户未中奖,同时删除错误的key, 已确保下一次请求重新计算总限制的值。
情况2:
同样如上的情况,用户只发来了一次请求,在自增操作之前key过期,而此次请求在自增之后判断是否正确的时候,又进来4次请求,该4次请求读取了错误的初始值1,同时进行自增操作,即尽管会被删除一回,但之后的4次会以1作为基础判断,第一个自增操作判断为错误并删除key,但234次操作均通过判断,则用户已中了6个奖。
所以一旦有人发现这一点,在0点时间戳附近时段大量刷新,则可能就会出问题。
针对这一情况,采用两个方法
1. 随机一个10秒到100秒的时间,加到key的过期时间中,避免时间戳直接暴露
2. 每次自增操作增加一次key的TTL判断,如果TTL小于10,则使用expire延长key的生存时间到下一个0点时间戳

更新,看了一遍memcached,觉得果然比redis更适合做缓存,支持多线程就不多说了,CAS操作依赖内建token而不是像redis一样依赖事务就能很完美解决我如上的抽奖需求,最起码会比redis的实现方案优雅很多。

17.11.3更新

实在抱歉没有在发表后仔细查看,我写文章的时候用的两个英文减号,犯懒没有上代码高亮,结果两个减号自动变成了一条横线。。导致执行错误,为大家说声对不起


 

首先来补个上篇redis文章的漏,我们在后台开启了redis,但是如何关闭呢?

假设没有更改端口,那么就使用如下语句:

redis-cli -p 6379 shutdown


下面来进入主题:

当我们应用docker的时候,docker内部的应用是可以直接通过宿主机IP来访问宿主机的,但是这种时候访问redis会被当做是远程访问了,而redis默认是关闭了远程访问的,那么首先我们需要开启,去哪里开启呢?自然还是在redis.conf中,上一篇文章已经介绍了这个文件,可以直接修改,当然我们这里还是使用’–‘语法来直接在启动时候改变。

但是首先还是来理解下redis.conf文件中如何关闭了远程访问,进入redis.conf,我们可以看到如下命令:

bind 127.0.0.1

就是这一条,限定了redis只接收本地端口的访问,看来我们只要更改了它的值就可以改变了,可是不要急,还有大段的注释我们没有看,当然这里我们就只截取对我们这个问题有用的:

# When protected mode is on and if:

#

# 1) The server is not binding explicitly to a set of addresses using the

#    “bind” directive.

# 2) No password is configured.

可以看到redis还存在protected mode,只要这个模式是开着的,就算更改了bind的值,如果没有设定密码的话,也无法开启远程访问:

protected-mode yes

那么如果不想设置密码,改成no就好了

所以直接这么写:

redis-server --bind 0.0.0.0 --protected-mode no  --daemonize yes

而远程访问就可以是:

redis-cli -h your ip -p port you set

可是连密码都不要,你的数据库是只要任何人试出来你的端口就可以随便访问了,这个肯定也是不行的,所以还是要设置个密码,再次去翻看redis.conf,可以看到如下命令被注释掉了

# requirepass foobared

这个词并没有意思,我第一反应是来自官方的恶意,fool + bared,愚蠢的裸露,意思是你们并没有设置密码,也挺有意思。只要解注释这一行,改一下就好了。当然我们会直接这么写:

redis-server --bind 0.0.0.0 --requirepass 1234567 --daemonize yes

已经设置了密码,就不必关闭protected-mode了

远程访问就变成了:

redis-cli -h your ip -p port you set -a your password

参考文献:

redis开启远程访问

redis安全设置