开发关键点:
1.JWT鉴权:相较于之前用户登录之后在服务端保存一个session的策略,JWT是无状态的,不需要在服务端保存任何数据,这样可以避免服务端的存储压力,提高系统的可伸缩性和性能。用户登录之后给用户签发一个Token,这个Token是根据用户的ID、Type和一个secretKey生成的。secretKey是由服务端设置的,用于JWT的解密和加密,只要secretKey不泄漏,就可以防止Token被伪造。
具体有以下代码实现:
func GenToken(userInfo *user.User) (string, error) {
c := MyClaims{
userInfo.Id,
userInfo.Type,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
Issuer: "kangning",
},
}
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 使用指定的secret签名并获得完成的编码后的字符串token
tokenStr, err := token.SignedString(MySecret)
if err != nil {
log.Panicln(err)
}
return tokenStr, err
}
// ParseToken 解析JWT
func ParseToken(tokenString string) (*MyClaims, error) {
// 解析token
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i any, err error) {
return MySecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
t, _ := Client.HGet(context.Background(), "black", strconv.Itoa(int(claims.ID))).Result()
if t == tokenString {
return nil, errors.New("更改密码后需重新登录")
}
return claims, nil
}
return nil, errors.New("JWT 解析错误")
}
2.redis分布式锁:在解决买票时发生的多个用户买同一张票的情况,考虑到性能和分布式等等原因,最终决定使用redis提供的分布式锁方案。该方案的核心操作是redis中的setNx操作,该操作的含义是添加一个键值对,当添加的key存在时当前操作失败,当key不存在时当前操作成功。添加键值对的操作对应的就是抢票这一行为,添加成功对应抢到票,添加失败对应没抢到票。
除此之外我们还要考虑一些情况。
- 用户买票完成之后理应释放分布式锁。但如果当前处理买票业务的节点内存占用过大导致OOM,致使当前进程被操作系统kill掉或者说你的云服务器不靠谱整个坏掉,这时必然是不能正常释放分布式锁了,为了解决这种问题,我们给锁加上了一个过期时间,可以是5s。
- 我们为锁加上过期时间后接踵而来的一个问题是,如果因为网络等问题,出现了离谱的延迟,甚至超过了5s,分布式锁默认会被释放,这时另一个进程/协程就可以再次抢到这张票,这很不合理吧,而且原来的任务执行完成之后会尝试释放分布式锁,这时它释放的分布式锁却是被人的锁,这下就乱套了,为了防止这种情况,我们创建一个后台协程为锁加上一个续期,当锁在邻近过期的时候还没有被主动释放,我们更新锁的过期时间,保证锁在正常情况下一直被持有。
- 我们这样做了之后,看似问题都解决了,但我们还忽略了一件事,我们默认redis是正常工作的,单节点运行的redis可能会成为一个单点问题。怎么解决呢,引入redis集群,官方有一个专门的称呼:“RedLock”(红锁算法)。我们假设我们有N个redis主节点,这些主节点相互独立。我们一次N个redis节点请求加锁,只有当至少(N/2+1)个节点加锁成功并且计算所总时长小于锁过期时间时我们才称加锁成功。释放锁时,我们依次释放N个节点中的分布式锁,是的,有些节点就算没有加锁成功我们也会对其发送一次释放锁的命令。(由于第二种策略已经能解决大部分问题并且第三种策略实现复杂度较高,故暂不实现第三种做法)
// AcquireLock 分布式锁,加锁
func AcquireLock(lockKey string) bool {
result, err := redisClient.SetNX(context.Background(), lockKey, 1, time.Duration(consts.RedisLockTimeOut)).Result()
if err != nil || !result {
return false
}
return true
}
// ReleaseLock 分布式锁,释放锁
func ReleaseLock(lockKey string) bool {
result, err := redisClient.Del(context.Background(), lockKey).Result()
if err != nil || result != 1 {
return false
}
return true
}
// LockRenewal 为分布式锁续期
func LockRenewal() {
var cursor uint64 = 0
ctx := context.Background()
for range time.Tick(1 * time.Second) {
keys, next, err := redisClient.Scan(ctx, cursor, "lock;*", 10000).Result()
if err != nil {
log.Println(err)
}
cursor = next
for _, key := range keys {
d, err := redisClient.TTL(ctx, key).Result()
if err != nil {
log.Println(err)
}
if d < 2*time.Second { //锁过期时间不足2s时,对锁进行续期
redisClient.Expire(ctx, key, consts.RedisLockTimeOut)
}
}
}
}
3.订单延迟支付:这部分实现的功能是用户下单之后,10分钟内需支付订单,若按时支付订单,则生成真实订单,若未及时支付,则取消订单。具体实现细节:订单服务收到用户抢票成功的消息后,根据票信息生成临时订单,并以订单信息为member,以10分钟之后的时间戳作为score,将该元素加入zset容器(ToDelayQueue)中。
- 先分析过期订单的处理流程,起一个后台协程(toTargetQueue)循环将小于当前时间戳的元素取出,并将member信息放入到一个目标List中,这个List起到一个缓冲的作用,后台再起一个协程执行(EventLoop)方法循环处理List中的订单信息。
- 分析按时支付订单流程,按时支付之后,从zset容器中根据订单信息和member删除对应的元素。(RemoveFromDelayQueue)具体代码如下:
部署后端之后,前端访问时,出现跨域问题,导致前后端一直无法交互,解决方法为使用nginx为前端发送来的所有请求加上一些http header信息使得前端能够成功访问后端。具体解决方法如下:
location /ttms/ {
proxy_pass http://127.0.0.1:8080/ttms/;
add_header Access-Control-Allow-Methods *;
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Max-Age 3600;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Headers $http_access_control_request_headers;
if ($request_method = OPTIONS) {
return 200;
}
proxy_set_header Host $host;
}