记录一些开发中踩过的坑~
2020.03.31 不能拉取test分支合并到自己分支
只能将代码往test分支合,不能将test分支合并到自己分支,否则会污染自己分支,导致报废。应该将自己代码在本地合并到test分支,解决冲突并且在本地环境测试通过后,push到测试环境测试,最后将自己分支合并到master,可以拉去master分支到自己分支。
2020.03.31 微服务是无状态的
直播截图。
我在这块最开始的思路是直播回调接口后,开一个协程去定时访问图片url。因为直播结束后,图片url就自动失效了,所以我根据响应的状态码和响应体去结束截图的循环。
问题:
如果因为网络原因导致请求失败,会出现直播正常,但是后台截图协程已经死亡。又或者主播暂时退出,也会导致截图url暂时失效。
另外要考虑服务是无状态的,单独起一个协程,当服务重启后,就不能继续截图了。
解决:
在离线统计服务里,写一个函数专门定时去截取正在直播的视频图片。在回调接口里,将流id和截图url存储到数据库中。离线服务定时获取所有正在直播的直播间sid,再从数据库读取每个直播间对应截图url,拉取图片,上传到oss,sid和图片id入库。
解决后的问题:
我写了一个函数,死循环去按照上面所说的方法实现,功能确实可以实现。但是又一个问题:服务将来会部署在多个实例上,这样每个实例都会去截图。
再次解决:
使用一个20秒的全局锁,每个实例的服务截图时,先获取锁,获取成功后进行一次截图。使用redis实现,设置一对键值,时长为20s。
2020.04.02 处理错误不要随便return
在离线统计服务日常数据服务里,会写一个死循环,在遇到错误时,打上日志,但是不能return!否则服务将会结束。
另外在controller处理请求时,遇到错误,也不能直接return,否则客户端将收不到响应。
2020.04.02 日志问题
日志的级别有info,debug,warn,error四个级别,Error类型的日志不能随便打印,如客户端参数错误,这个是客户端问题,不是服务端内部错误,不需要打印。因为生产环境下,一般会对error日志进行监控,不必要的错误日志会影响运维。
另外,打印日志需要打印出其上下文,方便debug,问题定位。
2020.04.10 .bash_profile启动问题
.bash_profile在终端打开时,不能自动source。
解决:
创建~/.profile文件,写入source ~/.bash_profile
2020.04.13 数据库查询语句索引问题
查询语句尽量命中索引,覆盖索引,减小数据库压力。
2020.04.13 python脚本读取mongo数据错误
1.编解码错误:读取的设备信息中存有特殊符号,需要进行utf-8编码。
2.值为null:读取的值可能为null,需要根据业务要求,设置合理的异常处理,或者过滤判断。
2020.04.13 vscode格式化代码慢
vscode自带的go格式化代码太慢,可以取消,写完项目代码后,提交版本前,使用在这里插入代码片gofmt -w src
命令格式化代码,再提交上传。
2020.04.14 Go程序函数return时候报错
变量作用域的问题,在子作用域定义一个上层作用域的同名的变量。
// 直接返回,并没有返回参数,在子作用域中,并不能直接返回上一层定义作用域中返回变量
// 也就是说命名参数返回,只能返回同一级别作用域下参数,不是同一级别作用域参数需要指明返回参数值
return
// 正确返回
//return err
2020.04.14 单个接口请求的失败不要导致整个请求失败
用户打开app时,调用一个live_config接口,获取配置信息和用户权限(包括开播,观播)。在其中调用用户权限的接口,如果返回500或其他,请求失败,应该将开播,观播权限设为false,而不应该返回客户端500错误,导致之后的配置信息也没获取到。
2020.04.15 数据库操作失败不能改变返回给用户的值
用户开播权限检查,根据mid进行放量,有权限的返回true,同时入库。此时入库失败的操作打印日志即可,不能返回权限为false。
2020.04.15 数据表字段值设计
在生产环境下,一般是不能删除用户数据的,所以对于数据表应该有一个状态字段,来标记该条信息为删除状态。有时候可以把状态字段与其他字段合并,比如用户观播权限,1 有,-1 无,0 待检查。如果没有0值,会导致用户权限确定后,没办法再检测。
2020.04.29 数据表外键
某张表有外键时,可以代码生成外键id,然后修改数据的id并插入连接表,再修改当前表的外键值为id。
2020.04.29 给返回的结果加开关
在返回给客户端信息的时候,可以加一个开关。
比如直播状态linve_on=1,取拉流地址,live_on=2,取录播地址。
2020.04.29 写controller时先做参数检查
2020.05.08 服务运行起来,但是端口未绑定
问题:已经可以从kafka读取消息并写入数据库,但是发布失败,端口绑定超时,没有成功。
debug:在main函数里打印多条日志,发现kafka初始化后面的日志为打印,确定问题在kafka初始化里。看代码发现,程序卡在kafka读消息的死循环里,导致后面的端口绑定没有执行到。
解决:将kafka初始化改成协程启动。
2020.05.08 数据库查询操作尽量一次查询完成,在内存里筛选
2020.05.09 consul里配置kv
问题:Consul里配置kv,在配置api时会设置apphost和appname,为啥?
因为apphost是域名,请求服务时需要先进行域名解析。而通过appname可以在consul里做服务发现,直接请求到服务。节省一次网络开销。
2020.05.12 代码上到正式环境后也要测试
在测试环境测试成功后,合代码到master后 ,根据修改的代码,在测试一遍相关功能。注意数据库表的修改!
2020.07.10 sql问题
SELECT mid, sum(balance) as recv_diamond FROM %s WHERE mid IN %s AND currency=? AND ct>=from_unixtime(?) AND ct
2020.07.11 http返回csv数据
场景: op后台需要支持运营人员的数据查询与下载
实现: 后台可以设置多种格式数据,然后修改http响应头供前端解析
fileName := fmt.Sprintf("主播数据.csv")
fileBody, err := setAnchorDataCsv(list, len(list))
if err != nil {
controller.ReplyServerError(c, ctx)
return
}
ctx.SetHeader("Content-Type", "text/csv")
ctx.SetHeader("Content-Disposition", fmt.Sprintf("attachment;filename=%s", fileName))
func setAnchorDataCsv(list []GetAnchorsDataResult, size int) (fileBody string, err error) {
file := &bytes.Buffer{
}
w := csv.NewWriter(file)
header := []string{
"主播MID", "主播Coco ID", "主播昵称", "主播国家", "主播签约时间", "所属公会", "公会建立时间", "水晶收入", "开播总时长(min)", "累计有效时长(≥30min)", "累计有效天(≥2h)"}
data := [][]string{
header,
}
// 时间模版
// timeLayout := "2006-01-02 15:04:05"
for i := 0; i < size; i++ {
context := []string{
strconv.FormatInt(list[i].Mid, 10),
list[i].Cid,
list[i].Name,
list[i].Country,
list[i].SigningTime,
list[i].GuildName,
list[i].GuildFound,
strconv.FormatInt(list[i].RecvDiamond, 10),
strconv.FormatInt(list[i].TotalTime/60, 10),
strconv.FormatInt(list[i].ValidTime/60, 10),
strconv.Itoa(list[i].ValidDay),
}
data = append(data, context)
}
err = w.WriteAll(data)
if err != nil {
logger.Error("csv write all data err: %s", err.Error())
return
}
w.Flush()
fileBody = file.String()
return
}
2020.07.22 举报封禁
场景: 用户被封禁后需要被禁止发言
设计:
set speak_ban_mid 1 24*60*60
hset forever_sepeak_ban mid
2020.07.23 敏感词过滤
场景: 主播添加房间敏感词后需要过滤掉相应发言
设计:
在下发用户消息的时候可以调用接口去判断是否为敏感词。判断时需要从redis里读,以免消息过多直接从mysql多导致穿透。
可以以key-value设置过期时间存储,也可以hash存储。value为json格式的主播敏感词数组。
2020.07.27 优化分页
场景: 需要分页下发给客户端列表内容
一般设计: 使用offset、limit做分页,缺点是需要客户端做计算。
生产设计:
使用一个nextcb对象,其有offset、more两个字段。客户端请求时不需要传offset,limit视情况而定。服务器处理时,先判断有无nextcb字段。
当客户端第一次请求时,默认offset=0,然后读库获取limit+1个数据,接着获取列表的实际长度len,如果len < limit +1表示再没有数据了,如果len==limit+1表示还有数据。创建nextcb对象,设置offset=len,more=0(0表示无数据,1表示有数据),和获取到的数据最后一同返回给客户端。为了方便客户端判断,可以在nextcb同级别加一个more字段跟nextcb.more保持一致,这样客户端就无须解析nextcb对象了。
当客户端第二次请求时,只需要将nextcb字段的值原封不动的传给服务器,服务器就可获得offset和more,如果more=0,直接返回空,否则同上面的情况一样。
对于客户端来说不用做计算,只需判断more是否为0,如果为0可以不发送请求,因为没有数据了。
优点: 简化了分页的计算,并且计算只在服务器端做控制,客户端只需要接收数据即可
2020.07.27 文案返回
场景: 当用户重复添加某一数据时,需要服务器查重并下发相应文案
设计:
INSERT INTO ... ON DUPLICATE KEY UPDATE ...
insert into room_sensitive_text set mid=1, sensitive_text='aaa' on duplicate key update is_del=0;
UPDATE room_sensitive_text SET sensitive_text=concat(sensitive_id, sensitive_text), is_del=1, ut=? WHERE sensitive_id=?
Error 1062:
错误。因此需要写一个工具方法,判断错误是否为如上错误,如果是则是重复添加,错误设为nil,如果不是就返回相应错误。方法如下:func IsMysqlDupKeyErr(err error) bool {
if err != nil && strings.Index(err.Error(), "Error 1062:") >= 0 {
//Duplicate entry
return true
}
return false
}
2020.07.29 sql问题
场景: 需要从表里获取到10个不同的被举报人uid,需要将举报人是vip用户的放在集合前。
问题sql:SELECT DISTINCT(uid) FROM table_name ORDER BY member_type DESC LIMIT 10 OFFSET 0;
**结果:**这个sql的初衷是想先按举报人身份member_type排序,然后distinct获取排序后的前10个不同uid。该sql读取出来的确实是10个不同的uid,但是并不是按vip举报优先排列的。
解决: 最后查了官方文档关于distinct的描述:在许多情况下结合ORDER BY的DISTINCT需要一个临时表。 所以试着将sql改成如下,解决了问题,记得给临时表加别名,否则会报错。
最终sql:SELECT DISTINCT(uid) FROM (SELECT * FROM table_name WHERE process_id=0 ORDER BY member_type DESC) a LIMIT 10 OFFSET 0;
2020.07.30 op运营人员操作记录
场景: 需要为运营人员的每一个操作增加记录,比如直播警告、强关,删除主播,处理举报等
设计: 可以整理一张总表,用来存放所有操作记录。表设计如下:
CREATE TABLE IF NOT EXISTS op_record ( operator VARCHAR(64) NOT NULL DEFAULT '' COMMENT '操作者', action_type TINYINT NOT NULL DEFAULT 0 COMMENT '操作类型', action_name VARCHAR(64) NOT NULL DEFAULT '' COMMENT '操作名字', extra text NOT NULL DEFAULT '' COMMENT '扩展字段', ct BIGINT NOT NULL DEFAULT 0 )ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = 'OP操作记录';
其中,extra为扩展字段,存放map,比如直播警告,可以在存sid、警告内容等。
2020.10.14 优化
问题:
现有的操作记录在调接口前需要封装参数map[string]interface{}
,过于麻烦
解决:
封装了一个添加操作记录的方法,根据反射拿到传入参数的值和json别名
func AddOpRecord(ctx context.Context, operator string, typ int, mid int64, param interface{
}) {
extra := make(map[string]interface{
})
ref := reflect.ValueOf(param)
typeof := ref.Type()
n := ref.NumField()
for i := 0; i < n; i++ {
var s interface{
}
f := ref.Field(i)
switch f.Kind() {
case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64:
s = f.Int()
case reflect.Float32, reflect.Float64:
s = f.Float()
case reflect.String:
s = f.String()
case reflect.Bool:
s = f.Bool()
default:
continue
}
extra[typeof.Field(i).Tag.Get("json")] = s
}
reacord := statsapi.AddOpRecordParam{
Operator: operator,
ActionType: typ,
Mid: mid,
Extra: extra,
}
statsapi.New(ctx).AddOpRecord(reacord)
return
}
2020.10.14 受leader订单ext存储扩展信息启发,再优化
问题:
第一次优化后的问题是后续每次去拿扩展字段内容时,需要强转类型。而leader的做法是将传入的整个结构体转为map的value,再起一个key名,这样在获取使用的时候,只需要param.(*ParamSt)
即可。
2020.08.03 mysql扩展
问题: NULL值转0,字符串截取,字符串转整形,CAST(… AS…)坑,format坑
解决:
SELECT IFNULL(NULL, 0); // 0
SELECT SUBSTRING("abcd",1,2); // ab
SELECT LEFT("abcd", 2); // ab
SELECT RIGHT("abcd", 2); // cd
SELECT CAST(0.7 AS SIGNED); // 1,四舍五入
SELECT CEILING(0.7); // 1,
SELECT FORMAT(12121323.12312,2); // 12,121,323.12,format大于1000会自动加‘,’进行分割
SELECT ROUND(12121323.12312,2); // 12121323.12,四舍五入
2020.08.07 时区问题
问题:
需要实时统计印尼直播每天的礼物、宝箱相关数据,比如消费人数、金额等,但是使用的是中国时区,但是0点到1点这段时间的数据因为时区原因没有统计上
解决:
有两个方案可以执行,第一种是 创建印尼时区的数据库连接,然后统计0点到24点的数据;第二种是 使用在中国时区的数据库连接,但是统计的是当日1点到次日1点的数据。
具体实现:
我采用的是第一种方法。
如何将本地时间的time类型转换成印尼时间的time类型呢?
// util.IDNLocation 是印尼的时区类型
// time.Time的In方法可以将本地时区时间转换成目标时区时间
t := time.Now().In(util.IDNLocation)
2020.08.10 服务接口更换问题
场景:
线上的接口设计不合理或者过期报废需要进行重构,且是微服务架构,存在服务间调用。
问题:
如果直接修改子服务的接口和上层服务的调用,那么在重新上线服务的时候,因为时间差的原因会导致存在一段时间的接口调用失败报错,可以说是一场事故了。需要考虑兼容性。
解决:
有二种解决办法。
(1)上层服务接口调用不变,只需要修改子服务逻辑或返回的字段。适用于接口名不需要改变,返回字段容易添加的接口。
(2)子服务先实现新的接口并上线,再修改上层服务的调用,并删除旧的子服务接口。适用于接口名需要改变,返回字段不容易修改的接口,基本可以解决大部分重构问题。
2020.09.10 git查看文件修改记录
先找到查看的行数,再执行下面命令
git blame init.go
2020.09.11 记第一次线上大事故
问题:
改了个功能,上线后,突然想起配置文件还没上传。所以有上传了配置文件,然后重启了线上服务,导致gateway服务挂了10min,在此期间没有发现。最后是运维人员孙红发现,进行了回滚。10min内广场刷不出直播。
解决:
因为重启服务的功能有问题,导致重启失败,服务一直尝试重启。回滚后,重新发布包,正常。
思考:
leader给我讲了一些上线的规范。
现在用户量小,所以像之前粗暴的merge后,直接发布上线,问题可能不是很大。但是随着用户量变大,这样做就有问题了。应该线上一台机器,然后观察线上的监控granfana,看各个指标都正常吗,包括cpu、接口耗时、内存占用、error数量等等有没有突变。正常后,再上另一台机器。
像最右那么大规模,还要进行放量,先放一部分实验,观察指标,再逐渐全量。
granfana可以选择服务,也可选择实例。
(1)逐渐放量
(2)单台机器部署
(3)观察指标监控
2020.09.14 内部接口批量查询数据
场景:
在上层服务里,会调用各个子服务接口批量获取一些信息,比如批量获取用户信息、主播信息、公会信息、礼物信息等。这个时候子服务接口返回list还是map呢
问题:
返回list,上次服务可能为了快速匹配信息,另外存到map当中;
返回map,上次服务可能为了返回给客户端信息,遍历存到list当中返回;
解决:
主要注意几个方面:
批量的大小、list是否返回给客户端、是否存在多重for循环
在我看来,只供内部访问的话,可以使用map,方便上层服务调用
2020.09.16 转盘下注结果返回
场景:
大转盘业务,当用户点击开始按钮后,服务端直接产生结果返回给h5,但是h5还需要做动效,延时展示结果
问题:
如果有人刻意抓包,就会发现当点击开始按钮后,直接就生成了结果,而不是等特效结束才产生结果。
解决:
(1)返回的字段名使用难理解的名字 比如 nc。。。约定成id
(2)对nc进行加密,再编码。本次使用的是aes堆成加密,约定好密钥和编码方式。
2020.09.21 幸运大转盘设计分析
场景:
幸运大转盘是上一周的产品需求,一个新的活动,由leader设计底层实现,我负责实现gateway上层接口。最后上线后,看了部分leader的源码,觉得设计思路值得学习,便记录一下,提高业务能力。
需求:
幸运大转盘分为普通场和豪华场。两场概率不同,中奖额度不同。每场转盘有12个奖品,有3个下注额度,分别为1抽、10抽、60抽。根据档位的不同,奖池也不同,比如一抽可能只选中12个奖品里的4个小奖。
另外,用户可以查看自己的中奖记录,并且有中奖信息的全服通报轮播。
分工:
leader负责写service层代码,包括结构体设计、付款、中奖逻辑、转盘奖池、多种类型奖品分发、以及奖品稀有度。
我负责写gateway层的接口,由H5直接调用我写的接口,我的接口再去调用底层leader实现的接口。主要有四个接口分别为 获取转盘信息接口、下注接口、中奖记录接口、中奖信息轮播接口。
底层实现:
转盘:
type LuckyWheel struct {
Id int64 `xorm:"autoincr pk" json:"id"`
ItemIds []int64 `json:"item_ids"` //存放12个奖品的id
WheelType LuckyWheelType `json:"wheel_type"` //字段用于区分普通场、豪华场。当存在多个同一类型的转盘时,选择ct最晚的返回。
Price int64 `json:"price"` //单注下注金额
Status int `json:"status"` //0:失效、1:有效
Ct time.Time `xorm:"created" json:"ct"`
Ut time.Time `xorm:"updated" json:"ut"`
Items []*LuckyWheelItem `xorm:"-" json:"items"`
}
奖品:
type LuckyWheelItem struct {
Id int64 `xorm:"autoincr pk" json:"id"`
ItemType LuckyWheelItemType `json:"item_type"` //奖品类型,水晶、礼物、碎片......
Key string `json:"key"` //可以是水晶数量、礼物id、碎片id......根据key查询奖品的详细信息
Level LuckyWheelItemLevel `json:"level"` //奖品稀有程度
Notice LuckyWheelItemNotice `json:"notice"` //中奖通知类型
Price int64 `json:"price"` //价值,用钻石衡量,方便风控
Weights []int64 `json:"weights"` //中奖权重,因为有三个档次,每个档次有一个当前奖品的权重,计算时就可以汇总权重,再随机
Ext map[string]interface{
} `json:"ext"` //扩展字段
Ct time.Time `xorm:"created" json:"ct"`
Ut time.Time `xorm:"updated" json:"ut"`
}
大转盘订单
const (
LuckyWheelOrderStatusInvalid = -1 //下注无效
LuckyWheelOrderStatusWait = 0 //等待下注扣款
LuckyWheelOrderStatusPaid = 1 //已支付
LuckyWheelOrderStatusAward = 2 //已发奖
)
type LuckyWheelOrder struct {
Id int64 `xorm:"autoincr pk" json:"id"`
Mid int64 `json:"mid"`
Sid int64 `json:"sid"`
WheelId int64 `json:"wheel_id"`
Amount int64 `json:"amount"` //下注金额
Status LuckyWheelOrderStatus `json:"status"`
AwardItemId int64 `json:"award_item_id"` //中奖item_id
PayOrderId int64 `json:"pay_order_id"` //下注支付订单id
PayType int `json:"pay_type"` // 下注支付方式 0钻石 1背包
AwardOrderId int64 `json:"award_order_id"` //中奖发奖订单id
Ext map[string]interface{
} `json:"ext"` //ext是常需的,方便业务扩展。比如item是多个碎片时,可以讲碎片的详细id和count写入进来。将来新的东西增加也可以通过 AwardItemId 查询 item类型,再根据类型解析ext
Ct time.Time `xorm:"created" json:"ct"`
Ut time.Time `xorm:"updated" json:"ut"`
}
下注逻辑:
type BetLuckyWheelParam struct {
Mid int64 `json:"mid"`
Sid int64 `json:"sid"`
WheelId int64 `json:"wheel_id"`
Amount int64 `json:"amount"`
PayType int `json:"pay_type"`
}
1、根据WheelId获取转盘信息,通过ItemIds查到该转盘的奖品详情。
2、Amount除以转盘单价Price,得到倍数scope
3、遍历item,取weights[scope]>0的作为奖池
4、累加奖池,随机生成数,判断rand5、选出奖品后,因为风控需求,所以这时要判断奖池里的钱是否够发,若不够,则改选择Price最小的item。此处就需要item的价值Price(礼物、碎片等风控)
6、如果奖品是碎片集合,还需随机选择碎片和数量
7、创建订单、未付款,将碎片信息写入Ext
8、支付钻石 或 免费优惠卷,两个逻辑
9、修改订单状态为已支付。此处要特别注意:**如果因为一些不可控因素(比如断电)导致修改状态失败,需要有离线计算任务去检查是否支付钻石或优惠卷,若支付过主动恢复
10、最后返回订单、奖品类型,碎片的话还需返回碎片id和count
离线订单计算:
1、按一定限制(比如3min以内wait状态、5hour以内paid状态)取出未完成的订单
2、检查wait状态订单是否已经支付,就是拿到orderid去exchang_order里查寻下注订单里的支付id是否有该oderid,如果有则说明支付过,修改wait状态为paid状态。如果是背包优惠卷同理。
3、对已经支付的订单做延时发奖,比如延时7秒,不能用sleep。如果7s内,就直接continue掉,下次计算再发奖。
4、因为奖品有钻石、礼物、碎片…所以需要调不同的方法完成发奖。主要包括2步:下发物品、修改订单状态为已发奖。
5、修改订单状态时需要建立事务,防止并发,造成多次发奖。
注意: 修改订单状态时使用xorm ForUpdate()方法设置读写锁,查询数据是若发现已有该锁,会等该锁释放
2020.09.28 加密结构体、struct与[]byte转换
场景:
一期大转盘只支持钻石,所以中奖只返回了id。二期需要支持礼物和碎片,因此中奖需要返回结构体并进行加密。结构体包含 奖品id、name、icon、count。
实现:
//将封装好的结构体json.Marshal即可
bs, err := json.Marshal(result)
nc, err := encryptByDes(bs)
2020.09.29 秋华老师分享会
1、Redis雪崩、一致性哈希
如果有六台机器,一个用户请求进来,根据hash,打到某一台机器上。当某一台机器挂点时,因为机器总数变了,导致每台机器上存储的id失效,导致直接访问数据库。当并发量大时,请求全落在数据库上,数据库必然扛不住、挂掉。
解决办法:使用一致性哈希。
在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对K/n 个关键字重新映射,其中K是关键字的数量,n是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。
2、raft算法、consul
3、service mesh
2020.10.10 通过日志计算pv、uv
场景:
大赢家活动结束后,产品想看大赢家活动页面的pv、uv以及其他一些数据。但是由于h5没有打点,所以需要我通过日志查看接口调用的情况。
解决:
因为之前有注意到leader做过,所以请教了leader。leader写了解析日志的awk文件,我只需要将我需要的日志grep出来,作为awk命令的输入就可以计算出调用接口的mid集合,即uv。
2020.10.12 离线计算sql问题
场景:
离线计算sql使用了大量的in、join、group by 、函数sum、max等,离线计算太慢,数据库压力过大
解决:
1、确保建立合适的索引
2、尽量不要大规模使用in、group by以及其他函数,因为离线计算对响应速度不要求很高,可以将数据从库中查出来,在内存里进行计算
3、in、 group by等操作规模过大时可以分批计算
2020.10.16 榜单计算
场景:
需要对现有的榜单进行优化,给用户展示日榜、周榜、月榜,同时增加积分相同时按获取时间先后排名
已有实现:
使用redis hash 和 字符串存储,hash存储用户积分,字符串存储前100list json字符串
缺点:
没有做到积分相同时按时间先后排名
解决:
方案一:可以在已有基础上,再维护一个hash 存放用户积分变动时间,在排序后再遍历比较一次时间,做下调整。缺点:当产品想要展示用户与前一名积分差值时不方便计算。
方案二(应用):使用redis zeset结构存储用户积分和mid。积分的整数部分为用户积分、小数部分为积分变动的最后时间戳。这样就不能直接使用zincrby()增量添加积分(因为小数部分设为了时间戳),需要在代码里先zrange()获取所有,再delete(key),最后zadd(key,map)。
2020.10.26 pymysql 踩坑
场景:
使用python实时计算土豪榜、水晶榜等积分榜,采用增量计算。但是测试时,无法获取实时更新的数据。
解决:
百度后,了解到 pymysql 每次查询后需要commit()。仔细阅读代码后,发现之前的一处改动,在execute()后,未调用commit()。修改后,解决bug。
2020.10.29 大转盘榜单积分计算
问题:
这次是大转盘榜单计算问题,因为在获取增量订单的时候使用的是ct和status=2。但是在任务运行的时候,获取到ct时间段的订单时,因为延迟发奖的原因,status可能没有立即修改为2。这样就会漏点一些订单,少计算积分。
解决:
将根据ct获取订单改成根据ut获取订单,这样就可以换取任意时间段内已经发奖的订单。
select * from lucky_wheel_order where status=2 and ct>=%s and ct<%s
2020.10.30 debug踩坑
问题:
在打印日志debug 的过程中,因为粗心看错了err打印的行数,导致一直在改同一行代码,而之前的错误同样的出现后面的一行中。自以为当前行没有修改正确…在改当前行的过程中,日志一直打印的错误一直没有变。
2020.10.30 error处理
背景:
获取榜单接口中,有一步是获取用户徽章信息。但是徽章不是必须返回的数据,所以当error或请求响应错误时,只是打日志,并不处理error,就有了下面的代码。
// 获取徽章信息
epauletResp, err := miscapi.New(ctx.TracingCtx).GetUserLiveEpaulet(miscapi.GetUserEpauletListParam{
MIDs: mids,
})
if err != nil || epauletResp.ErrCode != xcbase.ERRCODE_OK {
// 非关键数据.
logger.Warn("GetUserLiveEpaulet err %v resp %v", err, epauletResp)
}
问题:
乍一看好像没有问题,但却存在两个致命问题。
one:
如果这个err!=nil,后续的代码里也没有地方覆盖掉这个err,那么这个err将会被return到上层代码里。如果上次代码判断了err,那么会认为方法调用失败,返回错误响应!
two:
如果后续的代码里直接使用了epauletResp里的属性,如果属性是指针类型,那么直接使用属性时会导致panic。因为error时,并没有为指针类型分配内存。
解决:
有两个方案可以解决。
第一个方案如下,在打印完日志后,将err = nil。
第二个方案就是起另外的error变量名,比如err1,er等,不要覆盖要被返回的err变量值。
在后续使用epauletResp 指针属性时,先判断是否为nil。
// 获取徽章信息
epauletResp, err := miscapi.New(ctx.TracingCtx).GetUserLiveEpaulet(miscapi.GetUserEpauletListParam{
MIDs: mids,
})
if err != nil || epauletResp.ErrCode != xcbase.ERRCODE_OK {
// 非关键数据.
logger.Warn("GetUserLiveEpaulet err %v resp %v", err, epauletResp)
err = nil
}
if epauletResp.Data != nil && epauletResp.Data.Epaulets != nil {
epaulets := epauletResp.Data.Epaulets[args.MemId]
rtn.Self.LiveEpauletList = make([]*xcproto.EpauletListSt, 0)
for _, epaulet := range epaulets {
if epaulet == nil {
continue
}
rtn.Self.LiveEpauletList = append(rtn.Self.LiveEpauletList, &xcproto.EpauletListSt{
UrlImgDay: &epaulet.DayImgURL,
UrlImgNight: &epaulet.DayImgURL,
})
}
}
2020.11.05 离线任务设计
场景:
大转盘优化,新增每日充值领取一次机会免费转机会,并新增榜单
方案:
和leader商量后,采用实时+离线计算的方式下发优惠卷。实时可以提高实时效,离线计算用于兜底,以在一些不可控因素(上线、服务挂掉)发生时,也能够发放给用户优惠卷。充值也是采取的这样的模式。
代码实现遇到的问题:
实现:
离线任务每60秒全量计算当日每个用户充值的钻石数,往背包添加优惠卷时,直接插库,使用mysql的唯一key做判断,防止重复下发
问题:
这样实现会将所有的压力全部交给mysql
优化:
使用redis作缓存,如果给用户下发优惠卷后,将key存入mysql,并设置存在时间,满足充值条件时,先从redis获取key,若没有获取到再插库。mysql唯一key作为最后的一道唯一性保障。
方案二:
如果只有离线任务计算的话,其实也可以在for循环外面声明一个map和一个key。map存放已经下发的用户mid,key为当前日期。在for循环里判断,如果实时获取的当前日期 不等于 key,那么将key修改为当前日期,并将赋值给map新的地址。本次需求因为涉及实时计算,单次计算调用不到map,若将map在任务外创建,又会因为需要加分布式锁,变得更复杂。
实现:
使用redis存储,在充值额满足的条件下,再判断用户是否领取过,先从Redis GET key,若为nil,在执行add_bag_item。如果添加成功再Redis SET key。如果添加失败,需要再判断是否为唯一约束。如果是,也需要Redis SET key,因为Redis没有该用户,说明之前可能因为某些原因导致Redis SET 失败。
思考:
在给leader review时,leader说到为什么不使用 SET NX 操作,为什么先get再set,进行两次操作。接着leader又反应过来说,你这个需要添加背包物品成功后在set。
leader的说法让我有了一些思考。什么时候用 SET NX,什么时候用GET SET。
结果:
如果 只为了进行一次唯一性操作 or 不想覆盖原来数据,那么可以使用SET NX。
如果 对唯一性要求不高 or 只有满足条件时才set,那么可以使用GET SET。该操作存在并发问题。
实现:
在每一次的for循环里,包含对数据库的查询,我在每一次for循环里都创建了session,使用defer session.Close()
,并且在error的时候,傻逼的使用了return!!!(现在想想真是nc)
问题:
如果在for循环里创建session,并使用defer close,那么该方法永远不会结束,close永远不会起作用。并且每一次循环会丢失上次session的引用,并且未关闭,造成内存泄露。error的时候直接返回更是傻逼,任务会中断。
优化:
在for循环外城创建session,不要使用defer close。在遇到error时,主动关闭session等资源文件,再将session=nil等资源文件赋为空。先关闭,再置空。在循环体里第一步检查session是否为nil,如果为nil,则创建新的session。
// 实现
_, err := model.DeafultRedisManager.Do("GET", ukey)
if err != redis.ErrNil {
return
}
// 问题:傻逼的用请求redis操作的err返回去和redis.ErrNil判断,导致一直失败,进行不到后续的 add_bag_item操作 和 Redis SET。
// 因为此操作,造成上线后,用户充值后没有收到免费大转盘机会!!
// fixbug
resp, err := model.DeafultRedisManager.Do("GET", ukey)
_, err = redis.Bytes(resp, err)
if err != redis.ErrNil {
return
}
2020.11.10 odps跑数
场景:
今天leader让我从odps跑一下产品视频播放的ip、经纬度等数据。
实现一:
很容易想到的就是从网页登陆到odsp,然后写sql执行,得到结果。但过程中有两个问题。
问题一:网页查询只能查处10000条数据。
问题二:查询时因为我不知道atype的值所以就没设atype查询,只加了日期。所以查询时还是会遍历所有atype的分区。
实现二:
针对实现一里的问题二,我在查出来部分数据后,拿到了atype,完善了sql。
针对实现一离得问题一,我使用python写了简单脚本,迭代获取每行数据、打印。
然后就自信满满的在本地开始运行了,从11点一直执行到14点,才输出了3000w行。
这是我从写了另外一个sql,在网页执行,统计总共的行数,发现又1亿7千万数据。。。这样肯定不行。leader也在催我,让我在服务器上执行脚本。
我在服务器执行后,速度是能快3、4倍,速度还是不行。这样下去还是得好久。
实现三:
重新写可执行脚本,将sql查询的结果存到一张临时表里,在下载这张表,最后再删除这张表即可。
这样节省了一个一个迭代输出的io,转为网络下载的速度。最终花了10min下载完成,7.1GB。
#!/bin/bash
date=$1
if [ "$date" == "" ]; then
echo "Usage:$0 "
exit 1
fi
~/omg-odpscmd/bin/odpscmd -e "create table xl_tmp_ip as SELECT CASE WHEN GET_JSON_OBJECT(data, \"\$.ip\") IS NULL THEN 0 WHEN GET_JSON_OBJECT(data, \"\$.ip\") = '' THEN 0 ELSE GET_JSON_OBJECT(data, \" \$.ip\") END AS ip, CASE WHEN GET_JSON_OBJECT(data, \"\$.isp\") IS NULL THEN 0 WHEN GET_JSON_OBJECT(data, \"\$.isp\") = '' THEN 0 ELSE GET_JSON_OBJECT(data, \"\$.isp\") END AS isp, CASE WHEN GET_JSON_ OBJECT(data, \"\$.isp_srv\") IS NULL THEN 0 WHEN GET_JSON_OBJECT(data, \"\$.isp_srv\") = '' THEN 0 ELSE GET_JSON_OBJECT(data, \"\$.isp_srv\") END AS isp_srv, CASE WHEN GET_JSON_OBJECT(data, \"\$.lat\" ) IS NULL THEN 0 WHEN GET_JSON_OBJECT(data, \"\$.lat\") = '' THEN 0 ELSE GET_JSON_OBJECT(data, \"\$.lat\") END AS lat, CASE WHEN GET_JSON_OBJECT(data, \"\$.lon\") IS NULL THEN 0 WHEN GET_JSON_OBJECT(d ata, \"\$.lon\") = '' THEN 0 ELSE GET_JSON_OBJECT(data, \"\$.lon\") END AS lon FROM omg_data.omg_actionlog WHERE concat(ym,day)='$date' AND type='play' AND stype='video' AND atype='play-video';"
~/omg-odpscmd/bin/odpscmd -e "tunnel download xl_tmp_ip xl_tmp_ip"
~/omg-odpscmd/bin/odpscmd -e "drop table xl_tmp_ip"
2020.11.12 幸运礼物活动
背景:
11月中旬幸运礼物活动,新增加三种幸运礼物,同时调整中奖概率,由20%上升到37%,主播分成由40%降低到20%,PK、热度、日榜等计算都换成20%。实时更新榜单。
难点:
1、分散在各处的计算,PK值、热度等等计算修改,并且新老分成还是各自的比率计算。
2、保证前后端上线时的中奖概率、分成与规则所展示的同步。
解决:
1、在gift_order添加新的一列:主播分成比率share_rate,积分计算为:countpriceshare_rate/100,如果share_rate=0,则为40。
2、在概率计算前判断一下时间戳,如果小于活动开始时间则使用老概率,反之走新概率
2020.11.18 双月OKR制定
能力提升:
1、PUSH数据统计、分析、验证,加深PUSH整体业务理解
2、数据库优化、表设计能力
3、复杂模块、系统设计能力
复杂查询可以使用离线计算,再存储。
case:付费用户数据查询、日报;
订单相关需要有兜底,离线扫结算。
case:充值、大转盘抽奖、日充发放抽奖卷;
离线计算需要考虑分布式。
case:榜单计算(redis zset + 时间戳)、数据统计;
2021.1.12 crontab任务
corntab -e
0,5,10,15,20,25,30,35,40,45,50,55 * * * * cd ~/xl; python script.py
2021.1.28 redis的key都要设置过期时间
key一定要设置过期时间,即使过期时间设的很大。
2021.1.28 红点设置下发过的时机
需求:app新开了一个社区tab,需要向部分用户下发红点,点击后取消红点显示。
刚开始做的是,下发给用户红点,就写一个记录到readis,之后查看有记录就不再下发。这样做出现的问题就是用户可能没看到红点,也没点击tab,下次进去也还是看不见。
现在改为当用户点击tab,推荐流里的filter字段 等于"komunitas" 再设置下发过。