Redis常见的常见的数据结构特性
Set
普通set
是一种集合结构,存储的数据是无序的,常用的操作方法有:
- sadd
- spop
- smembers
具体的方法参数及返回值可以参考redis zh-cn doc
string
最简单的<key, value>
结构,Redis
的key
都必须是string
,且key
和value
的最大长度是512MB
,常用的方法有:
- get 读取制定key的value
- set
- getset:返回旧的value,并设置新的value,原子性操作。
- setnx:如果元素存在,返回0,如果不存在,则可以设置为想要的值,可以作为
分布式锁
- setex :设置指定key的value,同时设置过期时间,
非原子性操作
- incrby: 原子性增加制定key的value
hash
数据结构为 hash_key:field:value
,既先通过hash_key
找到对应的hash桶
,在根据field
找到对应field
的内容,常用的方法有:
- hincrby:可以对
不存在的key
调用此方法,默认值为0
,一次调用后变为1
- hset:设置,需要三个参数
- hget:读取,需要两个参数
- hsetnx:
原子性设置值
,如果元素存在,则不做任何操作,如果不存在,则设置,原子性,可以用作分布式锁
。
UV防刷方案
同一个用户或者多个用户高并发访问我们的页面时,如果我们要更新页面UV或者增加页面阅读数时,一般会用Redis来记录阅读数,本例给出一种用GO+REDIS实现的3s防刷的简单方案。
代码如下:
var LockTimeOut = 3
var SingleUserLockTime = 3
// 设置redis的分布式锁key,key可以由user-agent,user-id等能够代表个人信息的
var key4Lock = fmt.Sprintf("read_count:locker:article:%s:user:%s", articleId, userId)// redis string
var key4AllReadCount = fmt.Sprintf("read_count:permanently:article:%s:read_count", articleId)// redis hash
var key4ChangedArticle = fmt.Sprintf("read_count:temporarily:article:%s", articleId)//redis set
// 获取当前时间
var currentTime = time.Now().Unix()
var lockExpire = currentTime + SingleUserLockTime //SingleUserLockTime为多少秒的访问视为1次有效阅读数据
// 使用原子性的setnx尝试获取分布式锁
// 将Key4Locker对应的value设置为它应该过期的时间,防止高并发下我们的expire操作失败
lock, err := redis.Int(conn.Do("setnx", Key4Locker, lockExpire))
if err != nil {
// redis操作发生错误,这里需要记录一些日志信息
return
}
if lock == 1 {
// lock=1 表示已经当前goroutine已经成功获得了redis分布式锁
// 设置锁的过期时间,好让过期之后,当前用户的下一次访问能够正常地被记录到阅读数增加
_, err = conn.Do("expire", Key4Locker, SingleUserLockTime)
if err != nil {
// redis操作发生错误,这里需要记录一些日志信息
return
}
} else {
// 如果lock=0表示当前redis分布式锁已经被其他goroutine获取过了,需要等待过期
lockTime2, err := redis.Int64(conn.Do("get", Key4Locker))
if err != nil {
// 这里的错误和上面不一样
// 1. get操作可能会发生空返回,转为为redis.Int64会发生错误,导致这里的redis会发生非空
// 这种情况会发生在每秒钟同一个用户对同一篇文章的阅读数接口发生了上万次的调用,setnx执行没做任何操作,
// 下n个纳秒执行get时发生刚好发生空返回,这种情况我们可以认为当前这次请求发起时,距上一次不到3s,
// 可以直接return
// 2. redis链接失败发生错误,这里的err 也是非空的。
return
}
if currentTime < lockTime2+LockTimeOut {
// 如果取到当前redis中存储的过期时间,比较是否到了过期时间,如果当前时间还没到过期时间,则是刷阅读请求
// 如果超过了超期时间,需要检查是否是setnx之后的expire函数在高并发下调用失败
Logger.Info("判断为刷票行为")
return
}
// 如果当前时间已经到了分布式锁的超时时间但锁仍然没有过期,则需要给锁设置新的过期时间
lockTime3, err := redis.Int64(conn.Do("getset", Key4Locker, lockExpire))
//
if err != nil {
// redis操作发生错误,这里需要记录一些日志信息,这里也会存在空返回的情况
return
}
if currentTime < lockTime3+LockTimeOut {
// 如果当前时间还没到getset得到的过期时间,则判断当前请求是刷浏览数的请求
Logger.Info("判断为刷票行为")
return
}
// 如果确定超时了,重新设置过期时间为3秒
_, err = conn.Do("expire", Key4Locker, SingleUserLockTime)
if err != nil {
// redis操作发生错误,这里需要记录一些日志信息,这里也会存在空返回的情况
return
}
}
// end_region 处理是否是刷票行为
//#region 持久化存储
// 在redis中持久存储hash,设置当前articleId对应的文章的阅读数+1
_, err = conn.Do("hincrby", Key4AllReadCount, articleId, 1)
if err != nil {
// redis操作发生错误,这里需要记录一些日志信息,这里也会存在空返回的情况
return
}
//end-region
// 将articleId存入redis,表示当前article需要更新read_count
_, err = conn.Do("sadd", key4ChangedArticle, articleId)
if err != nil {
// redis操作发生错误,这里需要记录一些日志信息,这里也会存在空返回的情况
return
}
// 处理完成
// .... 加上自己的日志信息等