新年快乐
『大伙们开工了不 ?』
过年刚回来,前两天需求还没出,摸鱼正开心呢,需求来了!
需求描述
需求是给系统内的用户发薪,需要用户添加/修改银行卡,对用户的银行卡的信息进行校验,这个校验的接口是从阿里云上找的供应商,不太能白嫖
,每次请求这个接口收费1毛,但是预算有限,所以开发过程中产品强烈邀请增加一个校验:需要对每个用户添加/修改银行卡的操作进行限制,规则是每日每人三次只能校验三次银行卡。
概要设计
需求本身也不难理解,相信xdm看到这个需求都能想到很多的解决方案,例如
- 在数据库中存储用户Id及校验次数,定时任务根据每日时间定时删除。(比较繁琐,且不优雅,不推荐)
- 使用redis缓存,以userId生成不同的key,value初始化为0,每次添加/修改银行卡后加1,超过3次进行校验提示,根据业务需要对key设置过期时间(本次需求我使用的就是这个)
需求虽然简单,技术方案也比较明确,但是我好久没用redis了,这个项目也比较老,redis相关的配置也没有,开发过程中出现了不少的低级问题,以此文记录下,并鞭策下自己。
开发中的问题
RedisTemplate相关API不熟悉
之前也用过RedisTemplate,虽然印象有点模糊,但我蜜汁自信这么简单的需求难得倒我,就没有再去看看redis相关的命令,一把梭开始(以后再也不这样了,好好反省)。
首先每次调用请求进来,要做的是根据userId生成key,初始value的值为1,放进redis缓存里
为了方便本地测试,过期时间先不设置成1天,这里我是设置成50秒。
boolean existsFlag = this.redisTemplate.opsForValue().setIfAbsent("addBank_"+qry.getUserId()+"_count",1,50, TimeUnit.SECONDS);
Integer bindedDailyCounts = (Integer) redisTemplate.opsForValue().get("addBank_"+qry.getUserId()+"_count");
接着进行相关的银行卡校验之后,对这次userId对应的value进行+1的操作。
第一个问题就是下面我写的这个错误代码,使用了RedisTemplate的getAndSet()方法
this.redisTemplate.opsForValue().getAndSet("addBank_"+qry.getUserId()+"_count",bindedDailyCounts + 1);
当时的我还没意识到问题所在,一把梭完代码后,打开postman就是一顿请求,同一个userId,请求三次后,如愿以偿地看到了限制,这不就是产品同事要的效果嘛!
{
"code": 1010,
"data": null,
"message": "当日添加银行卡次数超过3次",
"taskId": "a0893c76ddda41f2bd9e27cee37c773f",
"time": "2022-02-09 18:05:08"
}
but!50秒之后我就使用同一个userId继续请求,按照设想,这个时候是没有限制的,结果居然还是被限制了。
{
"code": 1010,
"data": null,
"message": "当日添加银行卡次数超过3次",
"taskId": "8020e8ba0be144bfb0319888b2788ce5",
"time": "2022-02-09 18:12:27"
}
第一时间,我这过期时间设置的没错呀,setIfAbsent()方法,点击进去看看源码
/**
* Set {@code key} to hold the string {@code value} and expiration {@code timeout} if {@code key} is absent.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param timeout the key expiration timeout.
* @param unit must not be {@literal null}.
* @return {@literal null} when used in pipeline / transaction.
* @since 2.1
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
*/
@Nullable
Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);
看完之后,我的评价是我代码没问题啊,这个时候我还没意识到getAndSet()方法的问题,决定先打印这个key的过期时间看看。
System.out.println(this.redisTemplate.getExpire("addBank_"+qry.getUserId()+"_count") + " ---过期时间");
输出结果 -1,-1是什么意思,第一反应打开redis官网看看文档(https://redis.io/commands/ttl)
我key的过期时间呢,一行一行的代码看,嗯?getAndSet()
难道是它的问题,看了下源码,复习了下redis的命令,果然是它,由于自己对redis的不熟悉,非常不严谨的用了这个api,它会先拿到当前key,并且重新设置这个key,但是这个api没法设置过期时间,我当时根本没想到(再次鞭尸),当时心里主观认为,这个api就是拿出当前的key,然后再放回去,没想到这个key会被覆盖,导致拿不到之前设置的过期时间了。
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.ValueOperations#getAndSet(java.lang.Object, java.lang.Object)
*/
@Override
public V getAndSet(K key, V newValue) {
byte[] rawValue = rawValue(newValue);
return execute(new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.getSet(rawKey, rawValue);
}
}, true);
}
最后赶紧去复习下RedisTemplate和redis相关的命令,正确的增加使用次数的操作如下,在业务流程和银行卡校验走完后,使用increment()。
this.redisTemplate.opsForValue().increment("addBank_"+qry.getUserId()+"_count",bindedDailyCounts + 1);
删除这个key后,再次重复下之前的请求操作,发现50秒之后,可以对相同的userId再次操作了。
排查问题思路不对
接下来的问题和技术本身倒是没太大关系,问题也不大,主要是记录下自己排查问题的时候的思路,mark下自己思维有问题的点。
这个需求本地折腾完之后,自测了下接口,问题不大,使用IDEA一键发布到了测试机的docker上,然后开始和前端联调这个需求。
问题现象:添加银行卡的接口严重超时,前端调不通。我立马试了下本地发现没有问题,然后postman调用测试机发现确实超时调不通。
排查:首先我先看了一遍本地代码,发现没啥问题,重新发布了测试机,还是不行,折腾了很久才发现问题,docker内解析阿里云服务校验地址的域名居然和服务器解析的不一样。
解决办法:配置DNS强行映射到正确的IP。
排查过程中我的不足
- 丢到测试服务器上没有自测
- 定位问题时,日志没看全就以为知道问题是啥了,当时日志打印的很清楚,结果自己在网上一顿乱搜
- 开发过程中太过粗心(很严重)
总结
这次需求很简单,但实际开发过程中暴露了自身的很多问题,也不仅仅是这一个需求的问题,谨以此文鞭策自己,不断地温故而知新,不能连续两次掉进同一条河。