redis:使用go-redis实现用户注册登陆功能

语言: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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值