关于Redis的使用

这篇文章是有关我在工作中使用redis的一些总结,对于分布式锁使用的还是比较浅显,为了方便对这一部分知识的总结,开篇会介绍锁的基本内容,后续的使用仅针对我这一阶段工作使用的一个总结~
我所有的总结应该说都是一个初学者踩过来的路,更偏运用方面。当然我也知道运用应该建立在理论之上,由于进公司就开始写代码,所以现在处于这些知识有初步的认识,会用。后期也会在这条路上不断学习哒,所以之后学习到更深层次的东西也会再做知识的总结。

1.对Redis的认识

(1)是个数据库没错了:key-value数据库(类似哈希)
(2)分布式锁:就是防止进程之间相互干扰(用的比较多,下面会有栗子吃)
(3)适合什么情况:读取特别多,写入特别少。用它!没错

2.Redis的原理
redis原理

个人理解:有一个操作例如修改数据库中的某一条数据,线程1修改abc字段线程2修改bcd字段。那么如果两个线程同时进来,我们修改什么呢?
(1)首先如果加入redis锁,后进来的就会进行等待,直到先进来的线程处理完。让后再处理
(2)这个栗子犹如憨憨,因为即使不加锁我觉得也没事。sql是事务的,他依然会保留最后一条sql所执行的更改。但是这个锁可以防止两条一样的数据同时插入。不过也是画蛇添足,你在sql中加一个唯一索引!!!!
(3)回归正题:

  • 整体分析:你的第一个线程在开始执行就加一个锁,其他的线程进来之前先判断这个锁存不存在(获取锁),如果获取到这个锁就证明这个锁是存在的。也就证明有线程在,那我们需要等待(一般情况下,会做一个循环反复确认进去的线程结束没结束),当上一个线程结束时,它会释放掉他进来上的锁。这样在新线程循环检查到没有锁啦,他就进去再加锁。周而复始。

  • 分析如上的特殊词:
    <1>看门狗:其实就是每隔10s看一下锁还在不在,为了防止你的服务器突然当机了(就是死机了),这个锁一直在别的线程这辈子无缘的情况。其实避免这个情况,我们会为这个锁加一个过期时间。
    <2>hash算法选择一个点:哈希算法简单来说就是一个能快速找到我们要的数据的一种算法(具体的请自行科普)

3.Redis的类型
redis类型

个人理解:棕色快是我们现阶段可用的类型,绿色块是他的底层实现。绿色部门是会根据你存储的数据,redis自己底层用什么来存储你的数据。例如:

  • 压缩列表:当一个列表键只包含少量列表项,并且每个列表项要么就是小整数,要么就是长度比较短的字符串,Redis就会使用压缩列表来做列表键的底层实现
  • 整数集合:当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现
4.go操作Redis的实战

来个思路:初始化redis—》添加一个锁—》获得一个锁(上锁啦!)—》运行结束,删除这个锁(解锁)—》新进程进来要校验这个锁是否存在。到此一个redis操作结束。剩下的就是周而复始。
来个运用环境:我们在一个软件上想修改我们的登陆密码,我们要分步进行手机和邮箱的验证(验证码验证)。这种情况下redis是保证每一步是有关联的关键。当然我们上面所说的防止多进程进入也是栗子,但我觉得没这个有趣~
go实现—初始化:

// 初始化 
// 你那个json类型的config里面加入这个(你要是.go你就写个结构体也无妨)
"redis_config": {
    "addr": "127.0.0.1:6379",
    "password": "",
    "db": 0
  }

// InitRedis 初始化redis(写的一个方法最好不要写在main下面)
func InitRedis() error {
    // 加载你的配置文件
    conf := config.GetConfig()
    // 创建一个redis(别忘了给一个全局变量让别人方便调用)
   func InitRedis(_address []string, _prefix string) int {
    redisdb := redis.NewClusterClient(&redis.ClusterOptions{
        Addrs:    _address,
        PoolSize: 1000,
    })
    redisdb.ReloadState()
}

go实现—真实应用(分步锁):
思路:在校验手机验证码的时候上锁——》邮箱验证码校验之前删除锁后再加锁——》修改密码后删除锁
第一步:第一个校验

// 省略参数校验等前期准备工作(手机校验)
...
// 上锁,调用下面的方法进行上锁(request是接收前端参数的结构体)
if err := service.SetVerificationKey(string(request.TypeCode), request.PlatForm, request.UserId, authconst.JumpSecondVerification, logId); err != nil {
                return
            }

第二步:第二个校验

// 省略参数校验等前期准备工作(邮箱校验)
...
// 判断上一步的锁是否存在,就是取redis里面的值,如果值为0证明值不存在说明上一步校验失效
var v int
    if v, _ = service.GetRedisKeyToInt(resdis_key_utils.RedisKey.GetVerificationCodeKey(string(request.TypeCode), request.Platform, request.UserId, authconst.JumpSecondVerification)); v == 0 {
        return
    }
// 省略后的操作
...
// 删除上一步锁(request是接收前端参数的结构体)
service.DelVerificationKey(string(request.TypeCode), request.Platform, request.UserId, authconst.JumpSecondVerification)
// 重新加锁
if err := service.SetVerificationKey(string(request.TypeCode), request.Platform, request.UserId, authconst.JumpAction, v); err != nil {
        return
    }

第三步:变更密码操作

// 与上面类似,先判断上一步的锁是不是存在
// 存在进行变更操作
// 操作完毕,删除锁
package service
// SetVerificationKey 增加验证码校验成功后的锁 (拼锁的内容:操作+来源+id+下一步)
func SetVerificationKey(_type, _plat, _account string, _jump, devicesId int) (err error) {
    // 获得redis中的key
    key := resdis_key_utils.RedisKey.GetVerificationCodeKey(_type, _plat, _account, _jump)
    // 为这个key塞入value和过期时间
    err = coinredis.GetClient().Set(key, devicesId, redisconst.UserVerificationLockTime).Err()
    return err
}
// GetRegisterCodeKey 获取验证的key(操作+来源+id+下一步)根据自己的需求进行拼装,但要保证每一个锁的唯一性
func (r *redisKey) GetVerificationCodeKey(_action, _plat, _userId string, _jump int) string {
    return redisconst.UserVerificationNum + _action + _plat + _userId + strconv.Itoa(_jump)
}

作用:为什么要这么麻烦的一步一步上锁,那是因为我们需求是下一步再下一步分成不同页面来进行修改某数据的操作。防止有人越过前面的校验环节直接调用修改接口导致的不安全事件发生,所以我们为每一步上锁下一步判断的方式来进行。
另一种思路:我现在所实现的思路是在每步之间加锁,关心当前步和下一步。当然我们也可以在第一部就判断出需要经历的步骤,让后传入总的步骤和当前步骤。在最后一部的时候再进行判断。有想法的小秃头们可以试试。

5.Redis操作go的进阶实战

使用环境:当用户进行身份验证(验证码、登陆密码等),2小时内失败5次。用户将被锁定,不允许修改。
当然也可以是滑动2h(以最后一次失败往后延续2h)。类似的需求,大家可以自己为自己提出让后实现。

  • 准备工作:一张用户表中增加一个字段,1正常,2冻结
// 省略准备代码(例如:解析到结构题Request、身份验证等)
...
// 判断是否冻结 状态 1:正常 2:冻结
if !redisUtil.CheckRestPwdFailNum(user.UserId, user.Status) {
        user.Status = authconst.UserStatusFrost
        return 
    }
// 进行验证操作(成功返回true,不成功返回false)
result = VerificationCode(...)
// 如果验证操作失败
if !result {
    // 重置次数+1(如果=5或=3 改状态,删除key),返回
    _, err = redisUtil.IncrRestPwdFailNum(user.UserId)
    if err != nil {
        return codeconst.Code160000, codeconst.Msg160000, result, err
    }
}
// 如果登录成功,删除锁

    

// CheckRestPwdFailNum 判断用户是否冻结 true不是,false是
func CheckRestPwdFailNum(_account string, _status uint8) bool {
    // 获取redis的key
    key := resdis_key_utils.RedisKey.GetRestPwdFailNumKey(_account)
    // 常看key对应的value
    num, err := GetRedisKeyToInt(key)
    if err != nil {
        return true
    }
    // 如果已经是冻结
    if _status == authconst.UserStatusFrost{
        return false
    }
    // 失败次数作为value,判断次数是否大于你定义的最大次数
    if num >= authconst.UserRestPwdNum {
        // 更状态
        userM := &userdb.User{UserId: _account, Status: authconst.UserStatusFrost}
        if err = userM.UpdateUserById(nil, nil); err != nil {
            return false
        }
        // 变更状态后,删除锁
        DelRestPwdRedisKey(_account)
        return false
    }
    return true
}

// GetRestPwdFailNumKey 获取失败次数key
func (r *redisKey) GetRestPwdFailNumKey(_account string) string {
    return redisconst.UserRestPwdFailNum + _account
}

// GetRedisKeyToInt 获取redis值转int(获取value)
func GetRedisKeyToInt(_key string) (val int, err error) {
    return coinredis.GetClient().Get(_key).Int()
}

// DelRestPwdRedisKey 删除锁
func DelRestPwdRedisKey(_account string) {
    key := resdis_key_utils.RedisKey.GetRestPwdFailNumKey(_account)
    coinredis.GetClient().Del(key)
}

// IncrRestPwdFailNum 添加失败次数
func IncrRestPwdFailNum(_account string) (val int64, err error) {
    key := resdis_key_utils.RedisKey.GetRestPwdFailNumKey(_account)
    if val, err = IncrExRedisKey(key, redisconst.UserRestPwdMaxFailLockTime); err != nil {
        coinlog.GetLog().Error(fmt.Sprintf("incr RestPwdFailNum key %s error: %v", key, err))
        return
    }
    return
}

// IncrExRedisKey 自增并设置过期时间
func IncrExRedisKey(_key string, times time.Duration) (val int64, err error) {
    val, err = coinredis.Incr(_key) //1 nil
    if err != nil {
        coinlog.GetLog().Error(fmt.Sprintf("IncrExRedisKeyerr %s error: %v", _key, err))
        return
    }
    if err = coinredis.GetClient().Expire(_key, times).Err(); err != nil {
        coinlog.GetLog().Error(fmt.Sprintf("IncrExRedisKey %s error: %v", _key, err))
        return
    }
    return
}

总结一下:
这波操作就是:
(1)先判断锁的次数,如果到了最大次数,给他冻上清空次数。如果没大继续操作。
(2)进行正常验证
(3)验证失败,我为他加次数(获取对应的key,让后把他的value+1):IncrRestPwdFailNum调用IncrExRedisKey

推荐阅读更多精彩内容