语言:Go
数据库:redis
本实践只是用来练习redis数据库,实际中我们不会只使用redis来存储用户信息。
go语言库准备
HttpRouter构建Resuful
HttpRouter 是一个 Go 语言开发的轻量级高性能 HTTP 请求路由,也可以叫多路复用。
github地址:https://github.com/julienschmidt/httprouter
$ go get github.com/julienschmidt/httprouter
学习
go-redis操作数据库
github地址:https://github.com/go-redis/redis
文档:https://godoc.org/github.com/go-redis/redis
https://godoc.org/github.com/go-redis/redis#example-Client
$ go get -u github.com/go-redis/redis
具体使用方法请参考:https://segmentfault.com/a/1190000007078961#articleHeader3
数据库设计
用户注册&&登陆
- 使用用户名或者邮箱登陆
- 允许用户修改邮箱,不允许修改用户名
- 密码使用非明文存储
一个用户的信息可能有:
- 用户名 【唯一】
- 邮箱
- 密码 【非明文存储】
- 注册时间 【不需要注销时间,如果注销了直接删除就好】
- 手机等其他需要额外标注的信息
- 可扩展的字段(你不知道之后业务的形态发展,如果你不想连表查询的话)
因此我们可以使用HASH来存储用户的基本信息
(1)因为我们设计要求允许使用邮箱或者用户名登陆,因此我们不能使用邮箱或者用户名作为hash的key。因为我们如果使用用户名作为key,因为redis不可能获取所有hash表的email【因为我们不知道key】,就无法使用邮箱登陆了,邮箱同理。
那么如何确定用户的key。因为用户名唯一而且不允许修改因此可以可以使用一个自增长的ID作为hash表的key。
HSET VS HMSET
用户注册:
127.0.0.1:6379> hset userID:001 userName oceanstar email 123456789@qq.com password 123456 iphone 12345678 registertime "2019-03-05 15:39"
(integer) 5
127.0.0.1:6379> hgetall userID:001
1) "userName"
2) "oceanstar"
3) "email"
4) "123456789@qq.com"
5) "password"
6) "123456"
7) "iphone"
8) "12345678"
9) "registertime"
10) "2019-03-05 15:39"
127.0.0.1:6379> hset userID:001 sex woman -- 新增用户信息
(integer) 1
127.0.0.1:6379> hget userID:001 sex -- 获取用户某个字段的西悉尼
127.0.0.1:6379> HGET userID:001 userName
"oceanstar"
127.0.0.1:6379> HGET userID:001 email
"123456789@qq.com"
127.0.0.1:6379> hset userID:001 userName ocean -- 修改用户信息【逻辑层设置用户信息不可以修改】
(integer) 0
127.0.0.1:6379> hget userID:001 userName
"ocean"
127.0.0.1:6379> del userID:001
(integer) 1
用户注册:
127.0.0.1:6379> hmset userID:001 userName oceanstar email 123456789@qq.com password 123456 iphone 12345678 registertime "2019-03-05 15:39"
OK
27.0.0.1:6379> hmget userID:001 userName
1) "oceanstar"
127.0.0.1:6379> hmget userID:001 email
1) "123456789@qq.com"
127.0.0.1:6379> hgetall userID:001
1) "userName"
2) "oceanstar"
3) "email"
4) "123456789@qq.com"
5) "password"
6) "123456"
7) "iphone"
8) "12345678"
9) "registertime"
10) "2019-03-05 15:39"
127.0.0.1:6379> hmget userID:001
1) "oceanstar"
127.0.0.1:6379> del userID:001
(integer) 1
HSET 与HMSET 都可以用来存储信息。我查阅资料说HSET是设置一个field,HMSET是设置多个field,但是我感觉它们并没有什么区别。算了,我还是采用HMSET吧,不过听说hmset可以加快操作速度?
因为我们又要使用邮箱或者用户名登陆,其隐含条件是:
- 用户名不可同名 :hexists userID:001 userName
- 同一个邮箱只允许注册一次:hexists userID:001 email 【 查看散列的指定字段是否存在】
为了便于登陆,可以单独保存邮箱与用户id、用户名与用户ID的映射关系。
而每当用户登陆的时候,我们就可以:
- 通过邮箱,根据映射获取用户的id
- 与redis中存储的哈希数据对比邮箱和密码
- 用户名同理
因为是一对一,所以使用string类型。在注册邮箱-id,用户名-id表的时候,set命令中不会出现相同的值,所以我们只要保证key不相同就可以让邮箱-id,用户名-id出现一对一的关系。
密码存储:md5+盐存储
备注:exists userID:001 【查看指定key是否存在】
package main
import (
"crypto/md5"
"fmt"
"github.com/go-redis/redis"
"regexp"
"strconv"
"time"
)
func createClient() *redis.Client {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
// 通过 cient.Ping() 来检查是否成功连接到了 redis 服务器
_, err := client.Ping().Result()
if err != nil{
panic(err)
}
return client
}
func generate_Incr(client *redis.Client, incrName string)(incr_Key string){
result, err := client.Incr("incrName").Result()
if err != nil{
panic(err)
}
incr_Key = incrName + ":" + strconv.FormatInt(result, 10)
return incr_Key
}
func get_lately_Incr(client *redis.Client, incrName string)(incr_Key string){
incr_Key, err :=client.Get(incrName).Result()
if err != nil{
panic(err)
}
return incr_Key
}
func password_md5(password, salt string)string{
h := md5.New()
h.Write([]byte(salt + password))
return fmt.Sprintf("%x", h.Sum(nil))
}
type User struct{
userName string
email string
password string
iphone string
other string
}
func VerifyEmailFormat(email string) bool {
pattern := `\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*` //匹配电子邮箱
reg := regexp.MustCompile(pattern)
return reg.MatchString(email)
}
func register_user(client *redis.Client, user User)string{
if user.userName == "" || user.password == ""{
return "用户名或者密码不能为空"
}
if VerifyEmailFormat(user.email) == false {
return "邮箱格式不对"
}
nameKey := "userName:"+strings.ToLower(strings.TrimSpace(user.userName))
cmd, err := client.Exists(nameKey).Result()
if err != nil{
panic(err)
}
if 0 != cmd{
return "该用户名已经被注册"
}
emailkey := "email:"+user.email
cmd, err = client.Exists(emailkey).Result()
if err != nil{
panic(err)
}
if 0 != cmd{
return "该邮箱已经被注册"
}
uid_Key := generate_Incr(client, "userID")
sta, err := client.Set(nameKey,uid_Key, 0).Result()
if err != nil{
panic(err)
}
if sta != "OK"{
panic(sta)
}
sta, err = client.Set(emailkey,uid_Key, 0).Result()
if err != nil{
panic(err)
}
if sta != "OK"{
panic(sta)
}
mess := map[string]interface{}{
"userName":user.userName,
"email" : user.email,
"password": user.password,
"registertime" :time.Now().Format("2006-01-02 15:04:05"),
}
if user.iphone != ""{
mess["iphone"] = user.iphone
}
if user.other != ""{
mess["other"] = user.other
}
_, err = client.HMSet(uid_Key, mess).Result()
if err != nil{
panic(err)
}
return "register successfully"
}
func userLogin(client *redis.Client, userName , email, password, salt string)(isLogin, getName string){
if userName != ""{
uidKey, err := client.Get("userName:"+strings.ToLower(strings.TrimSpace(user.userName))).Result()
if err == redis.Nil{
return "userName does not exist", "00000000"
}
getpwd, err := client.HGet(uidKey, "password").Result()
if err != nil{
panic(err)
}
if getpwd == password_md5(password, salt){
return "login sucessfully", userName
}
}else if email != ""{
uidKey, err := client.Get("email:"+email).Result()
if err == redis.Nil{
return "email does not exist", "00000000"
}
getpwd, err := client.HGet(uidKey, "password").Result()
if err != nil{
panic(err)
}
if getpwd == password_md5(password, salt){
userName, err = client.HGet(uidKey, "userName").Result()
if err != nil{
panic(err)
}
return "login sucessfully", userName
}
}
return "fail", "00000000"
}
func register(client *redis.Client){
var newResister User
newResister.userName = "oceanstar"
newResister.email = "123456@qq.com"
newResister.password = password_md5("123456", "yan")
newResister.iphone = "18211112222"
status := register_user(client, newResister)
if status != "register successfully"{
panic(status)
}
}
func main(){
client := createClient()
fmt.Println(client)
register(client)
status, userName := userLogin(client,"oceanstar", "", "123456", "yan")
if status == "login sucessfully" {
fmt.Println(userName)
}
}
最近登陆的10个用户
需要收集的信息
- 用户名
- 登陆时间 【不需要存储,仅仅是逻辑思考】
思路:每次用户一登陆,就将用户名扔到list或者set中去。
set与list都提供一个列表的功能,它们之间的最大区别在于set中不可以有重复的元素,而且是无序存储的。
list:有顺序,可以重复的数据,但是我们可以在应用层去重
set:无顺序,不可以重复的数据
使用list实现
如果使用链表作为存储结构,没必要设置过期时间,如果当前登陆的人是之前就已经登陆人重新登陆,删除之前的并将这个用户名放到最新就行了,如果当前登陆的人不再链表中,直接放到最新,没必要判断链表上的用户还在不在线
127.0.0.1:6379> lpush recentlyLogin oceanstar 123 xingxing chenchen
127.0.0.1:6379> lrange recentlyLogin 0 -1 // 找出当前登陆的所有用户名
1) "chenchen"
2) "xingxing"
3) "123"
4) "oceanstar"
127.0.0.1:6379> llen recentlyLogin // 统计当前有多少人登陆
(integer) 4
如果链表的长度小于10,此时有新用户登陆
新用户已经在链表上了,就将原来的用户名删除,然后将新用户放到最新
新用户不在链表上,将新用户放到最新
如果链表的长度等于10
新用户已经在链表上了,就将原来的用户名删除,然后将新用户放到最新
新用户不在链表上,将最老的那一个删除,然后新用户放到最新
如果链表长度大于10
这是不可能的,说明程序有逻辑出错了
-- 新用户已经在链表上了
127.0.0.1:6379> lrem recentlyLogin 0 123 -- 删除指定用户
(integer) 1
127.0.0.1:6379> lrange recentlyLogin 0 -1
1) "chenchen"
2) "xingxing"
3) "oceanstar"
127.0.0.1:6379> lpush recentlyLogin 123
(integer) 4
127.0.0.1:6379> lrange recentlyLogin 0 -1
1) "123"
2) "chenchen"
3) "xingxing"
4) "oceanstar"
-- 链表的长度小于10,新用户不在链表上
127.0.0.1:6379> lpush recentlyLogin less10new
(integer) 5
127.0.0.1:6379> lrange recentlyLogin 0 -1
1) "less10new"
2) "123"
3) "chenchen"
4) "xingxing"
5) "oceanstar"
-- 如果链表的长度等于10,新用户不在链表上
127.0.0.1:6379> rpop recentlyLogin
"oceanstar"
127.0.0.1:6379> lrange recentlyLogin 0 -1
1) "less10new"
2) "123"
3) "chenchen"
4) "xingxing"
新用户放到最新
package main
import (
"fmt"
"github.com/go-redis/redis"
"strings"
)
func createClient() *redis.Client {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
// 通过 cient.Ping() 来检查是否成功连接到了 redis 服务器
_, err := client.Ping().Result()
if err != nil{
panic(err)
}
return client
}
func recentlyLogin_List(client *redis.Client, userName string)(result string){
val, err := client.LRange("recentlyLogin", 0, -1).Result()
if err != nil{
panic(err)
}
userName = strings.ToLower(strings.TrimSpace(userName))
for _, v := range val{
if v == userName{
_, err :=client.LRem("recentlyLogin", 0, userName).Result()
if err != nil{
panic(err)
}
_, err =client.LPush("recentlyLogin", userName).Result()
if err != nil{
panic(err)
}
return "ok"
}
}
num, err := client.LLen("recentlyLogin").Result()
if err != nil{
panic(err)
}
if num == 10 {
_, err = client.RPop("recentlyLogin").Result()
if err != nil{
panic(err)
}
}
_, err = client.LPush("recentlyLogin", userName).Result()
if err == nil{
return "ok"
}
return "false"
}
func get_recentlyLogin_List(client *redis.Client)(result []string){
men, _ := client.LRange("recentlyLogin", 0, -1).Result()
for _, v := range men {
result = append(result, v)
}
return
}
func main(){
client := createClient()
fmt.Println(client)
recentlyLogin_List(client, "oceanstar")
recentlyLogin_List(client, "Ocean")
recentlyLogin_List(client, "ocean")
recentlyLogin_List(client, "oCean8")
recentlyLogin_List(client, "oceanstar7")
recentlyLogin_List(client, "oceanstar6")
recentlyLogin_List(client, "oceanstar5")
recentlyLogin_List(client, "oceanstar4")
recentlyLogin_List(client, "oceanstar3")
recentlyLogin_List(client, "oceanstar2")
recentlyLogin_List(client, "oceanstar1")
fmt.Println(get_recentlyLogin_List(client))
}
set实现
set是没有顺序,不重复的string集合。
- 如果set只存储了用户名fail
当用户登陆时,
如果集合中元素小于10:
将用户名sadd这个集合,如果返回1当前集合中没有这个用户名,加入成功,返回0说明已经有了这个用户名,加入失败。
如果集合中元素等于10:
将用户名sadd这个集合,如果返回1当前集合中没有这个用户名,加入成功,返回0说明已经有了这个用户名,加入失败。但是我们无法得知哪一个是最先登陆的,也就是我们无法拿出最先登陆的哪个元素
127.0.0.1:6379> Scard recentlyLogin --计算有多少个元素
(integer) 1
127.0.0.1:6379> sadd recentlyLogin ocean -- 成功则返回1,不成功返回0
(integer) 1
127.0.0.1:6379> Smembers recentlyLogin -- 返回所有成功
1) "ocean"
- 因此,如果我们想要使用set完成这个功能,我们需要使用set存储用户名和一个标志登陆先后的标志,比如数字或者时间【使用时间作为标志,比较大小和替换标志的时候更加简单】,但是这样很麻烦,因为我需要取出所有的值,然后对这些值切割,再进行比较,十分麻烦。这也会给后续的程序设计带来麻烦
用户登陆是:
元素只可能小于等于10:取出所有成员,然后切割,然后比较是不是存在,如果存在,就销毁,然后重新插入。
集合元素小于10
如果不存在,那么就直接插入
集合元素等于10
如果不存在,就要找出时间最小的那一个,然后销毁它,插入新的值 【区别在于等不等于10】
127.0.0.1:6379> Smembers recentlyLogin
1) "ocean (@#$%^@) 2018-01-11 00:00:00"
127.0.0.1:6379> sadd recentlyLogin ocean&&2018-01-11 00:00:00
(integer) 1
package main
import (
"fmt"
"github.com/go-redis/redis"
"strings"
"time"
)
func createClient() *redis.Client {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
// 通过 cient.Ping() 来检查是否成功连接到了 redis 服务器
_, err := client.Ping().Result()
if err != nil{
panic(err)
}
return client
}
func recentlyLogin_Set(client *redis.Client , userName string)(result string){
userName = strings.TrimSpace(strings.ToLower(userName))
result = "false"
moreTime := "9999-99-99 99:99:99"
loginValue := userName +" (@#$%^@) " + time.Now().Format("2006-01-02 15:04:05")
var lessValue string
men, err := client.SMembers("recentlyLogin").Result()
if err != nil{
panic(err)
}
for _, v := range men{
temp := strings.Split(v, " (@#$%^@) ")
if temp[0] == userName{
num, _ := client.SRem("recentlyLogin", v).Result() //-- 销毁
if num == 0{
panic(err)
}
num, err = client.SAdd("recentlyLogin", loginValue).Result()
if num == 0{
panic(err)
}
return "ok"
}
if temp[1] < moreTime{
lessValue = v
}
}
num, err := client.SCard("recentlyLogin").Result()
if num == 10 {
client.SRem("recentlyLogin", lessValue) //不存在,销毁时间最小的那一个
}
num, err = client.SAdd("recentlyLogin", loginValue).Result()
if num == 0{
panic(err)
}
return "ok"
}
func get_recentlyLogin_Set(client *redis.Client)(result []string){
men, _ := client.SMembers("recentlyLogin").Result()
for _, v := range men {
temp := strings.Split(v, " (@#$%^@) ")
result = append(result, temp[0])
}
return
}
func main(){
client := createClient()
fmt.Println(client)
//str := "oceanstar (@#$%^@) 2019-03-06 17:24:53"
recentlyLogin_Set(client, "oceanstar")
recentlyLogin_Set(client, "Ocean")
recentlyLogin_Set(client, "ocean")
recentlyLogin_Set(client, "oCean")
recentlyLogin_Set(client, "oceanstar7")
recentlyLogin_Set(client, "oceanstar6")
recentlyLogin_Set(client, "oceanstar5")
recentlyLogin_Set(client, "oceanstar4")
recentlyLogin_Set(client, "oceanstar3")
recentlyLogin_Set(client, "oceanstar2")
recentlyLogin_Set(client, "oceanstar1")
fmt.Println(get_recentlyLogin_Set(client))
}
sorted_set实现【最简单】
sorted_set:用户名+登陆时间,但是有序集合的分值必须是浮点数,可以用时间戳来存储登陆时间。
当更新用户名时会自动更新值
package main
import (
"fmt"
"github.com/go-redis/redis"
"strconv"
"strings"
"time"
)
func createClient() *redis.Client {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
// 通过 cient.Ping() 来检查是否成功连接到了 redis 服务器
_, err := client.Ping().Result()
if err != nil{
panic(err)
}
return client
}
func recentlyLogin_sorted_set(client *redis.Client, userName string)(result string){
userName = strings.TrimSpace(strings.ToLower(userName))
var sorted_value redis.Z
i :=strconv.FormatInt(time.Now().UTC().Unix(), 10)
sorted_value.Score, _ = strconv.ParseFloat(i, 64)
sorted_value.Member = userName
client.ZAdd("recentlyLogin", sorted_value)
return
}
func get_recentlyLogin_sorted_set(client *redis.Client)(result []string){
val, err := client.ZRange("recentlyLogin", 0, 9).Result()
if err != nil{
panic(err)
}
fmt.Println(val)
return
}
func main(){
client := createClient()
fmt.Println(client)
recentlyLogin_sorted_set(client, "oceanstar")
recentlyLogin_sorted_set(client, "Ocean")
recentlyLogin_sorted_set(client, "oceanstar9")
recentlyLogin_sorted_set(client, "oceanstar8")
recentlyLogin_sorted_set(client, "oceanstar7")
recentlyLogin_sorted_set(client, "oceanstar6")
recentlyLogin_sorted_set(client, "oceanstar5")
recentlyLogin_sorted_set(client, "oceanstar4")
recentlyLogin_sorted_set(client, "oceanstar3")
recentlyLogin_sorted_set(client, "oceanstar2")
recentlyLogin_sorted_set(client, "oceanstar1")
fmt.Println(get_recentlyLogin_sorted_set(client))
}
备注:
127.0.0.1:6379> keys * // 找出所有的key
127.0.0.1:6379> Flushall // 清除数据库中的所有key
参考:https://www.jianshu.com/p/2670a08d146a