如何绕过ChatGPT针对服务器提供商的IP封禁

今天早上一起来,发现新App审核又被拒了,但理由又是他们找不到IAP项目,遂怀疑是他们对汉语引导的理解问题,写了一段非常详尽的引导,然后打开ChatGPT,想翻译成英文,结果发现ChatGPT把我的IP ban了。

一开始,还以为也是其他人遇到的账号被封问题,但仔细一看是禁止了我的IP访问。我一直是用自建在Vultr上的服务器来进行专业上网的,虽然价格可能比某些服务贵些,但贵在稳定和安心。(5刀一个月,1000G流量,其实也还能接受了)

于是我在Vultr上直接新建了一个服务器,启动好之后配置一下,发现依然不行,不论是ipv6还是ipv4,也都不行,于是我想到估计是Vultr整体被封了,去搜了一下,才发现原来我还算是幸运的了,早两个月开始ChatGPT就已经开始大规模封禁来自各个云服务提供商的请求了,aws,GCP,Vultr这些大的提供商更是早就上了黑名单。但搜索之下,也找到了应对的办法,那就是利用魔法打败魔法。

ChatGPT是利用cloudflare来进行网络防护的,而cloudflare家自己,却出了一个安全上网和反嗅探的工具:Cloudflare Warp,也就是著名的1.1.1.1。

利用这个工具,我们只要在服务器端配置好了,既可以实现穿越ChatGPT的封锁。(注意,是在服务器端安装,在你本机安装并没有用,所以如果你并不是自有服务器,那么后续的内容对你来说可能用处就不大了)

下面是具体的操作步骤:

首先,登录你的服务器(比如ssh或者在云服务的官网找对应工具),在命令行里输入:

如果你的服务器是基于apt的(例如Ubuntu或者Debian)
sudo apt install cloudflare-warp
如果你服务器是基于yum的(例如centOS或者RHEL)
sudo yum install cloudflare-warp
安装好之后,继续运行如下的代码:
warp-cli register
如果成功,会显示一个Success

接下来这步比较关键,而且其实是有限制的一步,因为一旦开启warp之后,我们本地对服务器的访问,其实也会被限制,我目前只找到了一个添加例外IP的做法(其实还有个同样是cloudflare提供的zero trust的将你的服务器和本机组成类似内网的解决方案,但这个方案我个人是感觉限制过多而且过于依赖cloudflare了,万一哪天它也反了,就。。)

但添加例外IP,其实就是将本机的IP地址添加到warp的例外中,而我们都知道,除非你自己拉了根企业光纤有独立的IP地址,否则家用宽带的IP地址,就是一直在变的,这个也就是限制所在了,但一是我们的IP不会那么快变,二是连接之后只要你不断开,IP变了也是可以继续使用的,三是warp的例外支持网段,我们可以尽可能的扩大例外的规模,来减少我们失去例外的机会。

那么接下里就是去百度搜一下IP地址,找到你本机目前的IP,然后在服务器的命令行里,输入:
warp-cli add-excluded-route xx.xx.xx.xx
其中xx.xx.xx.xx就是你的IP地址,假如你想加入一个IP网段范围来减小失效的概率,可以将其改为:
xx.xx.0.0/16
注意!!!:上面这个网段,只是一个示例,表示从xx.xx.0.1到xx.xx.255.254之间的所有IP地址。在使用时,请确保里你理解这其中的风险以及网段的具体意义。(当然,允许这些IP访问不代表你的原本的其他安全鉴定会失效,这里只是针对warp的限制例外)
在完成上述步骤之后,可以运行下面的代码来开启warp(再次注意:如果你没添加上述规则,你的ssh将无法再连接,你只能通过云服务提供商提供的方法来连接了):
warp-cli connect
开启成功后,可以使用下面的代码来检查是否真正启动:
curl https://www.cloudflare.com/cdn-cgi/trace/
如果你看到返回的字段中,有warp=on存在,那就是开启成功了,下面你就可以继续请求访问ChatGPT了

一些可能遇到的问题和个人经验(及广告):

安装问题

在使用apt或apt-get安装时,返回了Unable to locate package的提示,那么可以考虑先运行
sudo apt update
来更新你的资源列表,但如果更新之后依然不行(比如我),那么cloudflare也提供了手动下载的方式,你可以在这个页面中找到对应的Linux版本的warp安装包:

https://pkg.cloudflareclient.com/packages/cloudflare-warp 找到对应的包之后,可以下载之后,利用注入scp之类的工具拷贝的服务器上,也可以复制下载链接,然后在服务器中运行:
curl -o name-of-your-file https://you-download-url
然后再进行安装,比如我的机器是Debian的,那么接下来我会运行:
sudo dpkg -i name-of-your-file
sudo apt-get install -f
然后就安装完毕了

IP网段问题

如果你还是觉得添加例外这个方法不靠谱,那么你可以继续研究cloudflare的zero trust方案,也欢迎你推出新的教程并告诉我,下面是对应的文档地址:

https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/use_cases/ssh/

服务器和专业上网(广告)

其实现在来说,影袜的方案,就我自己的经验来看,是非常容易被嗅探到的,尤其是那些常用的海外云服务,之前经常就是这个服务器我刚刚开好,安装好了影袜,结果没几分钟就被封了,而且是明显IP能ping通,但不论你怎么换端口也都不行了,但现在我用的方案,非常简单,无需安装额外的工具,而且只要IP能ping通,基本上就能稳定使用。那就是使用ssh自带的动态端口转发功能,只需要一行指令:
ssh -N -g -D 1088 username@xx.xx.xx.xx
即可在你本地的1088端口开放一个通道,如果你只是希望自己的本机使用,那么可以去掉-g指令,或者显式的声明127.0.0.1:1088,如果不加,则其实同个局域网的其他设备,也可以访问你的IP:1088来进行专业上网,比如手机上,可以直接用小火箭来访问,稍微修改下配置即可。

然后以Mac为例,则可以在自己的设置-网络-高级-你懂得里配置诸如socks或者http方案,以及例外域名或者PAC方案。当然,如果你想要方便的管理这些配置而不是每次都打开设置来操作,可以尝试下我开发的Mac App:

https://apps.apple.com/cn/app/proxyho/id6444635008?mt=12
当然ssh的方案,因为加密算法的原因,其实理论速度是会不如专门为了这个而生的影袜的,但个人感觉这些损耗可以忽略不计,毕竟安逸了许多

另外如果你看了本篇文章,希望选择服务器,那么我也推荐一下Vultr,我个人认为性价比还可以,而且它们的官网并不需要特殊的姿势就可以直接访问,也很友好,下面是我的推广链接,服务器都是按使用时长计费的,试用不满意,关了就好了。当然他们家用的人很多,那么就也会偶尔出现IP被ban的情况,但因为是按照时长收费的,遇到这种情况,关闭服务器再重新启动一个即可。

https://www.vultr.com/?ref=9418691-8H

对了,他们家还支持ipv6网络方案,也无需额外付费,只要勾选即可

Docker container容器内访问宿主机host服务

问题:docker的container内,本身是一个微小的主机,那么请求127.0.0.1或者localhost,自然是请求到了container本身的网络,而无法抵达宿主机。

在以往,需要各位去手动寻找docker创建的docker0 bridge来访问宿主机网络,但在今天,docker已经提供了非常优雅的解决办法了

Linux:

Docker版本高于v20.10(2020年12月4日更新)

在启动docker时,加入如下语句
--add-host=host.docker.internal:host-gateway
而在container内,可以直接请求host.docker.internal:PORT,来获取宿主机上提供的各种服务

如果使用了Docker Compose,则应该将下面的句子加入container的声明中:

extra_hosts:
- "host.docker.internal:host-gateway"

Mac和Windows:

Docker版本高于v18.03(2018年3月21日更新)

直接在container内使用host.docker.internal:PORT来访问宿主机服务即可

对于Mac上Docker版本低于上述版本的:

Mac Docker版本v17.12到v18.02: 使用docker.for.mac.host.internal

Mac Docker版本v17.06到v18.11: 使用docker.for.mac.localhost

对于更低版本的docker,只能使用老旧的方法了,这里不再多做赘述

参考:https://stackoverflow.com/a/43541732

使用SSH来连接远程服务器的MySQL

本文要解决什么问题:当远程服务器无法开放MySQL端口或者无权限操作远程服务器的安全组时,我们应该如何在本地连接到远程的MySQL并进行操作

本文基础要求:远程服务器至少开放了SSH端口(通常默认情况下端口号为22),如果未开放22端口,则本文所述方法并不可用。

本文方法总结:利用SSH自带的端口映射功能,建立一个tunnel,将远程服务器上的MySQL端口映射到本地的端口上,从而使得本地客户端可以直接连接

关于如何使用SSH,以及如何更简单的使用SSH,可以参考另一篇文章

当SSH已经准备就绪,打开Terminal,输入如下指令


ssh -L [local port]:[database host]:[remote port] [username]@[remote host]

ssh -L [本地端口]:[数据库所在地址]:[数据库远程端口] [远程服务器用户名]@[远程服务器地址]

*如果你不用同时登陆远程服务器进行操作,可以加上-N指令

以上指令唯一需要解释的就是数据库所在地址,如果远程服务器本身就是MySQL服务器,那么可以直接填127.0.0.1, 而如果远程服务器本身还是一个跳板,则应该输入在远程服务器内可以访问到MySQL的地址。

示例如下:

ssh -N -L 3310:127.0.0.1:3306 root@MyRemote

以上的命令会将远程服务器监听3306端口的MySQL服务,映射到本地的3310端口上,之后可以直接使用MySQL客户端在命令行里登录,或者使用pma等数据库操作工具

mysql -u root -h 127.0.0.1 -P 3310 -p

利用context和kubectx快速切换kubernetes集群

在使用kubectl操作kubernetes集群的过程中,可能会遇到需要操作不同集群的问题,例如本地的minikube集群和线上的部署集群,或是线上的测试集群和线上的部署集群,如果集群之间切换不频繁,也可以使用笨办法,比如准备多个config文件在/.kube文件夹下,要切换的时候就改config的名字。不过对于需要频繁切换操作集群的人来说,这个方法显然太麻烦了,当然也不够cool。

当然,k8s是非常完善的集群解决方案,肯定是考虑到了这个问题的,这里就用到了k8s的config文件的context(上下文)了。

如果你是从零开始配置两个集群的cluster, user等信息的,那么可以参照官方的这个文档

Define-clusters-users-and-contexts

这里就不详细说以上这种方法了,我们来说说已经存在了两个集群的config文件时如何简单的生成context,当然,在上述文档的后半部分,也有我要说明的这些,也可以去看英文原文,我这里是因为遇到了一个坑,所以详细解释一下。

首先说方法,我们都知道kubectl如果没有设置config相关位置的环境变量,那么其会默认去一个地方寻找,对于Mac来说,就是

$HOME/.kube/config

如果要自己指定一个config文件所在的位置,那么就要设置KUBECONFIG环境变量,例如:

export KUBECONFIG=$KUBECONFIG:$HOME/.kube/config

而如果有两个集群的配置文件呢,其实就可以非常简单的直接加在环境变量的后面,然后让kubectl自动为我们生成context,例如:

export KUBECONFIG=$KUBECONFIG:$HOME/.kube/config:$HOME/.kube/config-2

注意:可以使用

kubectl config view

来查看目前的context的情况

那么坑在哪里呢,由于两个集群的配置文件可能是管理员只分配给你自己使用的,那么用户名就可能是相同的,在用户名相同的情况下,如果集群的连接启用了TLS验证,那么kubectl自己融合的config文件就会使用相同的client-certificate-data和client-key-data,如果两个集群在同一服务商的同一片网络下还好,可是如果是在不同的网络下,那么就会出现只能连接上其中一个,而另一个集群无法连接的情况,这种时候,我的解决办法是修改用户名,但要注意,在config里的context下,可以看到user和name两个字段,我们要修改的是user字段,这里只是一个名称,不影响集群侧的用户验证,修改完user字段,记得翻到users字段下,那里面有一个name,这个name的值应该和刚刚修改之前的user字段中的值一样,把这里也修改成同样的新值,保存,重新配置环境变量使之生效,这时候就可以使用了。

至于官方提供的切换集群的方法,在上述文档中也能找到,这里也不在赘述,这里主要介绍一个第三方工具来帮我们更快捷的做这个事:
kubectx

这个工具可以给context指定alias并快速的切换他们,简单的介绍一下

kubectx                     : 列出目前所有的context
kubectx <NAME>              : 切换到<NAME>对应的context
kubectx -                   : 切换到上一次的context
kubectx <NEW NAME>=<NAME>   : 给<NAME>context命名一个新名字
kubectx <NAME>=.            : 给当前context命名一个新名字

可以看出,在完成了上述config文件自动融合之后,使用这个工具就可以快速的切换不同的操作集群,同时这个工具还支持bash/zsh的tab自动补全,以及fzf这个快速选择工具,可以说从此以后切换集群就可以如丝般顺滑了,不过缺点就是kubectx这个工具会影响对kubectl的自动补全,全看各位的习惯了
 

Mysql更新排名的取巧实现

最近的项目连续遇到了对用户排名的需求,一个要求不用并列,一个要求同分并列,因为用户体量的考虑,拒绝了实时更新(即每个用户查询排名时获取排名),改为定时更新,在网上查了大量的MySQL排名算法之后,发现大部分的文章是以查询时获取来实现,而非定时更新所有人的排名,所以经过一番研究之后自己写了一个算法,欢迎斧正

首先我们假设有一张user表,其中有四个字段,score,rank,score_rank,join_time分别代表用户的分数,排名,获取排名时的分数,及用户加入时间

首先是不考虑并列的情况

SET @r = 0
UPDATE user
SET `rank` = @r := (@r + 1), `score_rank` = `score`
WHERE `score` != 0
ORDER BY `score` DESC, `join_time` DESC // 因为没有并列,所以增加一个排序

然后是考虑并列的情况

SET @r = 0;
SET @l = 0;
SET @s = 1;
UPDATE `user` SET `rank` = (
CASE
WHEN `score` = @l THEN @r := @r + (@s := @s + 1) - @s
ELSE @r := (@r + @s) + (@s := 1) - 1 END), `score_rank` = @l := (`score`)
WHERE `score` != 0
ORDER BY `score` DESC

这里唯一需要注意的事情就是MySQL中的变量问题,变量只能在两个地方赋值,SET或SELECT时,其余的地方只允许在变量赋值给其他字段时才允许被修改,所以就是在这里我们开始取巧,来解释一下并列时的思想:

r,l,s分别是当前排名,当前排名的分数,和当前同分的人数

当某一行的数据与上一行的分数相同时,即 score = @l,那么这时应该给rank赋值为当前的排名,但同时应该更新同分人数,所以我们这样来实现:@r := @r + (@s := @s + 1) – @s,这样s实现了自增,但对返回结果r没有影响

当某一行的数据与上一行的分数不同时,则应该给rank赋值r+1,但同时我们希望给l赋值当前的分数,并把同分人数s归为1,则选择这样实现:@r := (@r + @s) + (@s := 1) – 1

关于速度

这个算法在我这边的模拟测试中,三万条的数据排序用时0.4秒,所以肯定不能作为实时处理,计划是每天定时更新一回,操作时间也算是可以接受了。不知道其他算法的时间是会是多少,欢迎提供下对比。

Golang生产环境中time包的zonefile.zip问题

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

    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")

升级MacOS10.13 High Sierra导致PHPMyAdmin无法正常显示的问题

升级了新的版本之后,一切介正常,唯独发现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-Go 交叉编译运行Go程序

最近在学习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缓存设计

最近开始做一个通用抽奖的项目,项目中有一个活动用户每天的抽奖限制和总中奖限制,即每天最多抽多少回和活动期间总共中多少个。这一判断的特定就是请求量巨大,肯定是不能直接依靠关系数据库来做判断的,所以就要用到了缓存,将每一位用户的这两个限制都写到缓存中,需要判断限制的时候,直接读缓存就好。
首先先否定内存缓存,因为尽管缓存的数据是非常小的,但是参与抽奖的用户数量会很大,更何况一个应用会服务多个活动,那么用户数量更加不可估计,内存缓存很容易挤爆并丢失数据。
当然还有一个原因就是环境,项目的环境是容器,多容器之前无法共享内存缓存中的数据,而抽奖限制要求又必须是唯一的,所以内存缓存只能抛弃。
于是就来到了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的实现方案优雅很多。