一.go-redis操作hash
常用命令:
redisClient.HSet("map", "name", "jack")
// 批量设置
redisClient.HMSet("map", map[string]interface{}{"a": "b", "c": "d", "e": "f"})
// 单个访问
redisClient.HGet("map", "a").Val()
// 批量访问
redisClient.HMGet("map", "a", "b").Val()
// 获取整个map
redisClient.HGetAll("map").Val()
// 删除map的一个字段
redisClient.HDel("map", "a")
// 判断字段是否存在
redisClient.HExists("map", "a")
// 获取所有的map的键
redisClient.HKeys("map")
// 获取map长度
redisClient.HLen("map")
// 遍历map中的键值对
redisClient.HScan("map", 0, "", 1)
Hash
的常用场景主要有两种:
缓存对象
做购物车
首先是缓存对象,hash命令中的key
,value
,filed
很好的能够对应对象的结构我们可以利用Hash
来缓存结构,比如像下面我写了一个json
文件,我们来看如何将它缓存起来:
[
{
"message":{
"name": "张三",
"age": 30,
"email": "zhangsan@example.com",
"isStudent": false,
"subjects": ["数学", "英语", "物理"]
}
},
{
"message": {
"name": "李四",
"age": 25,
"email": "lisi@example.com",
"isStudent": true,
"subjects": ["化学", "生物"]
}
}
]
缓存的代码如下:
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
snoyflake "go-redis/sonyflake"
"io"
"os"
"strconv"
)
type Student struct {
id uint64
Message map[string]any `json:"message"`
}
var rdb *redis.Client
var ctx context.Context
func Init() {
rdb = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
err := snoyflake.Init()
if err != nil {
fmt.Println("sonyflake init failed,err:", err)
return
}
ctx = context.Background()
}
func main() {
//初始化
Init()
var students []Student
//读取json文件
file, err := os.Open("student.json")
if err != nil {
fmt.Println("file Open failed,err:", err)
}
defer file.Close()
str, _ := io.ReadAll(file)
err = json.Unmarshal(str, &students)
if err != nil {
fmt.Println("json Unmarshal failed,err:", err)
return
}
//生成id
for _, student := range students {
student.id, _ = snoyflake.GetID()
}
//写入redis
filed := "message"
for _, student := range students {
value, _ := json.Marshal(student.Message)
err = rdb.HSet(ctx, strconv.FormatUint(student.id, 10), filed, value).Err()
if err != nil {
fmt.Println("redis HSet failed,err:", err)
return
}
}
//读取redis
for _, student := range students {
value, err := rdb.HGet(ctx, strconv.FormatUint(student.id, 10), filed).Result()
if err != nil {
fmt.Println("redis HGet failed,err:", err)
return
}
fmt.Println(value)
}
}
运行结果:
{"age":25,"email":"lisi@example.com","isStudent":true,"name":"李四","subjects":["化学","生物"]}
{"age":25,"email":"lisi@example.com","isStudent":true,"name":"李四","subjects":["化学","生物"]}
那购物车我们应该怎么做呢?假设我们现在要清空我们的购物车,购物车其实就三个属性:
谁买
买什么
买多少个
知道了这个我们就可以尝试实现一个简单的购物车了,这里我们选择将Redis
和Mysql
联合使用,将商品的具体信息储存在Mysql
中,Redis
中只实现购物车的相关功能:
首先我们生成一个表来存储商品信息:
然后我们现在模拟一个场景:
- 我们将商品将入购物车
- 我们添加购物车中商品的数量
- 计算总价格
这个基本上就是一整个大致流程了,我们来看一下怎么实现:
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
snoyflake "go-redis/sonyflake"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"io"
"os"
"strconv"
"time"
)
type MySQL struct {
DataName string
DataUser string
DataPassword string
DataHost string
DataPort string
}
type Redis struct {
Addr string
Password string
DB int
}
type Setting struct {
MySQL MySQL
Redis Redis
}
// 用户
type User struct {
UserID int `gorm:"primaryKey;autoIncrement"`
Price float64 `gorm:"type:float;not null"`
}
// 商品
type Goods struct {
GoodsID int `gorm:"primaryKey;autoIncrement"`
ProductName string `gorm:"type:varchar(255);not null"`
Category string `gorm:"type:varchar(50);not null"`
Brand string `gorm:"type:varchar(50);not null"`
Price float64 `gorm:"type:decimal(12,2);not null"`
StockQuantity int `gorm:"not null"`
Description string `gorm:"type:text"`
ListingDate string `gorm:"type:date"`
}
type Message struct {
Userid int `json:"用户ID"`
Goodsid int `json:"商品ID"`
Number int `json:"数量"`
}
var ConfMessage = new(Setting)
var db *gorm.DB
var rdb *redis.Client
var ctx context.Context
var messages []Message
var userlist []string
var costlist map[string]float64 = make(map[string]float64)
func main() {
//初始化
Init()
//读取json文件
file, err := os.Open("shopping.json")
if err != nil {
fmt.Println("file Open failed,err:", err)
}
defer file.Close()
str, _ := io.ReadAll(file)
err = json.Unmarshal(str, &messages)
if err != nil {
fmt.Println("json Unmarshal failed,err:", err)
return
}
//写入redis
for _, message := range messages {
userlist = append(userlist, strconv.Itoa(message.Userid))
filed := strconv.Itoa(message.Goodsid)
err = rdb.HSet(ctx, strconv.Itoa(message.Userid), filed, message.Number).Err()
if err != nil {
fmt.Println("redis HSet failed,err:", err)
return
}
}
//选择其中某种商品添加一定数量
rdb.HIncrBy(ctx, "1001", "1001", 3)
//打印一下查看是否操作成功
fmt.Println(rdb.HGetAll(ctx, "1001").Val())
//计算总价格
for _, userid := range userlist {
cost := TotalPrice(userid)
costlist[userid] = cost
}
for k, v := range costlist {
fmt.Println(k, v)
}
}
func TotalPrice(userid string) float64 {
var user User
res, _ := rdb.HGetAll(ctx, userid).Result()
for k, v := range res {
id, _ := strconv.Atoi(k)
number, _ := strconv.Atoi(v)
goods := Goods{}
db.Where("goods_id = ?", id).First(&goods)
db.Where("user_id = ?", userid).First(&user)
user.Price += goods.Price * float64(number)
}
return user.Price
}
func InitConfig() error {
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("ini")
err := viper.ReadInConfig()
if err != nil {
return err
}
if err = viper.Unmarshal(&ConfMessage); err != nil {
return err
}
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
if err = viper.Unmarshal(&ConfMessage); err != nil {
return
}
})
return nil
}
// 初始化redis
func InitRedis() error {
rdb = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
ctx = context.Background()
_, err := rdb.Ping(ctx).Result()
if err != nil {
return err
}
return nil
}
// 初始化mysql
func InitMysql() error {
dns := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local",
ConfMessage.MySQL.DataUser,
ConfMessage.MySQL.DataPassword,
ConfMessage.MySQL.DataHost,
ConfMessage.MySQL.DataPort,
ConfMessage.MySQL.DataName)
var err error
db, err = gorm.Open(mysql.Open(dns), &gorm.Config{
//跳过默认事务,提高性能
SkipDefaultTransaction: true,
//禁用外键约束
DisableForeignKeyConstraintWhenMigrating: true,
NamingStrategy: schema.NamingStrategy{
//禁用默认表名复数
SingularTable: true,
},
})
if err != nil {
fmt.Println("连接数据库失败", err)
os.Exit(1)
}
var sqlDB *sql.DB
sqlDB, err = db.DB()
if err != nil {
return err
}
_ = db.AutoMigrate(&User{}, &Goods{})
//设置连接池最大连接数量
sqlDB.SetMaxOpenConns(100)
//设置连接池最大空闲连接数
sqlDB.SetMaxIdleConns(10)
//设置连接连接可重用的最大时长
sqlDB.SetConnMaxLifetime(10 * time.Second)
return nil
}
func Init() {
err := snoyflake.Init()
if err != nil {
fmt.Println("sonyflake init failed,err:", err)
return
}
err = InitConfig()
if err != nil {
fmt.Println("config init failed,err:", err)
return
}
err = InitMysql()
if err != nil {
fmt.Println("mysql init failed,err:", err)
return
}
err = InitRedis()
if err != nil {
fmt.Println("redis init failed,err:", err)
return
}
}
这个代码有点长我们根据main
函数中的逻辑来顺一下:
-
Init
:首先在这个函数我们完成了对相关工具的初始化,主要有以下几步:
1.初始化雪花算法
2.初始化相关配置,这里我们选择的是viper
来读取配置文件
3.初始化redis
4.利用gorm连接mysql
数据库,完成对`mysql的初始化 -
读取json文件
:这里我们将相关的信息存储在json
文件中来模拟购物车初始消息:json
文件内容如下:
[
{
"用户ID": 1001,
"商品ID": 1001,
"数量": 1
},
{
"用户ID": 1001,
"商品ID": 1003,
"数量": 1
},
{
"用户ID": 1002,
"商品ID": 1002,
"数量": 1
},
{
"用户ID": 1002,
"商品ID": 1004,
"数量": 1
},
{
"用户ID": 1002,
"商品ID": 1005,
"数量": 1
}
]
我们将读取后存储到redis来作为对相关信息的缓存(如果大家想更贴合实际环境,可以添加一个过期时间)
相关操作
:最后我们模拟了我们平时增加/减少购买数量的操作并且通过解析redis
中的相关信息并在mysql
中查询实现了结账操作,完成了一个购物车的基本功能。
二.go-redis操作Set
常用命令:
// 往一个集合里面添加元素
redisClient.SAdd("set", "a", "b", "c")
// 获取集合中的所有成员
redisClient.SMembers("set")
// 判断一个元素是否属于这个集合
redisClient.SIsMember("set", "a")
// 随机返回count个元素
redisClient.SRandMemberN("set", 1)
// 获取一个集合的元素个数
redisClient.SCard("set")
// 获取集合中的所有成员
redisClient.SMembers("set")
// 判断一个元素是否属于这个集合
redisClient.SIsMember("set", "a")
// 随机返回count个元素
redisClient.SRandMemberN("set", 1)
// 获取一个集合的元素个数
redisClient.SCard("set")
// 弹出并删除该元素
redisClient.SPop("set")
// 弹出并删除N给元素
redisClient.SPopN("set", 2)
// 从源集合移动指定元素刀目标集合
redisClient.SMove("set", "set2", "a")
// 删除指定元素
redisClient.SRem("set", "a", "b")
// 遍历集合
redisClient.SScan("set", 0, "", 2)
集合主要有以下的特性:
- 无序
- 无重复的元素
- 支持并交差等操作
比较适合用来数据去重和保障数据的唯一性,还可以用来统计多个集合的交集、错集和并集等,当我们存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储。
注意: Set
进行聚合计算(交集、差集、并集)时复杂度较大(>=N),在数据量比较大的时候,任意造成Redis实例阻塞,为了解决这种情况我们一般会选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。
让我们在使用Set
中一般会在以下场景中使用:
点赞
共同关注
抽奖
这里我们以点赞功能为例,我们来看一下我们可以如何实现一个点赞功能:
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
var (
rdb *redis.Client
ctx context.Context
)
func main() {
err := InitRedis()
if err != nil {
fmt.Println("redis init failed,err:", err)
}
Upvote("1", "1")
Upvote("2", "1")
Upvote("3", "1")
str, _ := GetUpvote("1")
fmt.Println(str)
count, _ := GetUpvoteCount("1")
fmt.Println(count)
CancelUpvote("1", "1")
str, _ = GetUpvote("1")
fmt.Println(str)
count, _ = GetUpvoteCount("1")
fmt.Println(count)
}
// Upvote 点赞
func Upvote(userid, articleid string) error {
return rdb.SAdd(ctx, articleid, userid).Err()
}
// CancelUpvote 取消点赞
func CancelUpvote(userid, articleid string) error {
return rdb.SRem(ctx, articleid, userid).Err()
}
// GetUpvoteCount 获取点赞数
func GetUpvoteCount(articleid string) (int64, error) {
return rdb.SCard(ctx, articleid).Result()
}
// GetUpvote 获取点赞列表
func GetUpvote(articleid string) ([]string, error) {
return rdb.SMembers(ctx, articleid).Result()
}
// GetVoteStatus 获取点赞状态
func GetVoteStatus(userid, articleid string) (bool, error) {
return rdb.SIsMember(ctx, articleid, userid).Result()
}
func InitRedis() error {
rdb = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
ctx = context.Background()
_, err := rdb.Ping(ctx).Result()
if err != nil {
return err
}
return nil
}
go-redis操作zset
常用命令如下:
// 往有序集合中加入元素
redisClient.ZAdd("ss", redis.Z{
Score: 1,
Member: "a",
}, redis.Z{
Score: 2,
Member: "b",
})
// 返回有序集合中该元素的排名,从低到高排列
redisClient.ZRank("ss", "1")
// 返回有序集合中该元素的排名,从高到低排列
redisClient.ZRevRank("ss", "1")
// 返回介于min和max之间的成员数量
redisClient.ZCount("ss", "1", "2")
// 返回对元素的权值
redisClient.ZScore("ss", "a")
// 返回指定区间的元素
redisClient.ZRange("ss", 1, 2)
// 返回介于min和max之间的所有成员列表
redisClient.ZRangeByScore("ss", redis.ZRangeBy{
Min: "1",
Max: "2",
Offset: 0,
Count: 1,
})
// 给一个对应的元素增加相应的权值
redisClient.ZIncr("ss", redis.Z{
Score: 2,
Member: "b",
})
// 删除指定元素
redisClient.ZRem("ss", "a")
// 删除指定排名区间的元素
redisClient.ZRemRangeByRank("ss", 1, 2)
// 删除权值在min和max区间的元素
redisClient.ZRemRangeByScore("ss", "1", "2")
Zset 类型(Sorted Set,有序集合) 可以根据元素的权重来排序,我们可以自己来决定每个元素的权重值。在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,可以优先考虑使用 Sorted Set。