Redis一个开源的,是高性能的 key-value 数据库,单线程,所有操作均为原子操作。可以用来进行高性能的内存缓存,例如高度重复的数据库查询、秒杀活动中的库存防超卖、消息队列、数据基数统计、距离数据索引/查询等等。本文先从基本的数据结构入手,进行实际应用。
Redis有如下几种数据类型:
* String
* Hash
* List
* Set
* Sorted Set
* Pub/Sub
* Geo
* HyperLogLog
* Bitmap
* Bitfields
* Stream (Redis 5.0以后)
什么?其他博文上为啥都说只有5种数据类型?严格意义上来说也没错,Redis最初的基础数据类型就是只有前5种,但是后面陆续添加了几种数据类型,严格意义来讲pub/sub并不是一种数据类型,而是 Redis 的一种消息传递模式,这里不过多赘述,在后续写Redis消息队列的时候详细说明。本文先就前5种数据类型的实际应用进行详细说明。
本文所有实例代码均基于golang和go-redis(v9)库
String是Redis中最简单、最常用的数据类型,它是一个二进制安全的字符串,可以包含任何数据,比如jpg图片或序列化的对象等。实际上Redis种所有直接存储的数据都需要被转化为字节数组存储为String类型或直接存储为String类型。
String类型的特点是单个键值对的最大存储空间为512MB,可以通过set、get等命令进行读写操作。
缓存常用数据,如用户信息、文章内容等,以提高访问速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | func testString() { // 测试string类型 stringKey := "UserInfo_709394" result, err := redisClient.Set( context.Background(), stringKey, "{\"user_id\": \"709394\", \"user_name\": \"尼古拉斯-赵四\", \"age\": 48}", time.Second*2, // 缓存2秒 ).Result() if err != nil { log.Fatalln(err.Error()) } fmt.Println("Set result:", result) result, err = redisClient.Get(context.Background(), stringKey).Result() if err != nil { fmt.Println(err.Error()) } fmt.Println("Get result:", result) time.Sleep(time.Second * 3) // 等待缓存过期 result, err = redisClient.Get(context.Background(), stringKey).Result() if err != nil { fmt.Println("Get error:", err.Error()) } fmt.Println("Get result:", result) } |
执行结果:
1 2 3 4 | Set result: OK Get result: {"user_id": "709394", "user_name": "尼古拉斯-赵四", "age": 48} Get error: redis: nil Get result: |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | // PackageListOutPut 门店套餐列表返回值 type PackageListOutPut struct { PackageID string `json:"package_id" orm:"column(package_id)"` UniqueMark string `json:"unique_mark" orm:"column(unique_mark)"` PackageSubject string `json:"package_subject" orm:"column(package_subject)"` PackageSummary string `json:"package_summary" orm:"column(package_summary)"` SortMark string `json:"sort_mark" orm:"column(sort_mark)"` CreateTime string `json:"create_time" orm:"column(create_time)"` ModifyTime string `json:"modify_time" orm:"column(modify_time)"` CoverImage string `json:"cover_image" orm:"column(cover_image)"` FixedPrice string `json:"fixed_price" orm:"column(fixed_price)"` EstimatedPrice string `json:"estimated_price" orm:"column(estimated_price)"` AttachAction string `json:"attach_action" orm:"column(attach_action)"` PackageType string `json:"package_type" orm:"column(package_type)"` } // 实现 encoding.BinaryMarshaler 接口 func (m *PackageListOutPut) UnmarshalBinary(data []byte) error { return json.Unmarshal(data, m) } func (m *PackageListOutPut) MarshalBinary() (data []byte, err error) { return json.Marshal(m) } func testStruct() { structStoreKey := "PackageListOutPut_store_id_100001" toStoreStruct := PackageListOutPut{ PackageID: "709394", UniqueMark: "bedee0e6-834d-4593-997c-aea9b58d0286", PackageSubject: "美容洗护套餐", PackageSummary: "深层清洁,进制提拉", SortMark: "100", CreateTime: "1672599845000", ModifyTime: "1672599845000", CoverImage: "https://gjh.me/wp-content/uploads/2015/12/IMG_0003.jpg", FixedPrice: "168.00", EstimatedPrice: "299.0", AttachAction: "1", PackageType: "3", } result, err := redisClient.Set( context.Background(), structStoreKey, &toStoreStruct, time.Second*30, ).Result() if err != nil { log.Fatalln(err.Error()) } fmt.Println("Set result:", result) var structFromRedis PackageListOutPut err = redisClient.Get(context.Background(), structStoreKey).Scan(&structFromRedis) if err != nil { fmt.Println(err.Error()) } fmt.Println("Get result:", structFromRedis) } |
输出结果:
1 2 | Set result: OK Get result: {709394 bedee0e6-834d-4593-997c-aea9b58d0286 美容洗护套餐 深层清洁,进制提拉 100 1672599845000 1672599845000 https://gjh.me/wp-content/uploads/2015/12/IMG_0003.jpg 168.00 299.0 1 3} |
这里需要注意的是几乎所有的数据都能够转换为字节数组存储到Redis中,然后读取后也可以很方便的将字节数组序列化到原始的数据格式中。例如golang中实现 encoding.BinaryMarshaler 接口即可。
通过incr/decr命令实现自增/自减操作,如网站访问量统计。
通过incr/decr命令还能利用Redis的原子+单线程特性实现秒杀库存的精确减少,从而避免超卖,后续写分布式锁再详细讨论。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | func testNumber() { // 测试数字 numberKey := "TodayPV" result, err := redisClient.Set(context.Background(), numberKey, 0, time.Second*86400).Result() if err != nil { log.Fatalln(err.Error()) } fmt.Println(result) wg := sync.WaitGroup{} for i := 0; i < 10_086; i++ { wg.Add(1) clickCount := int64(i%5 + 1) go func(count int64) { // 模拟并发访问的时候增加PV _, err := redisClient.IncrBy(context.Background(), numberKey, count).Result() if err != nil { log.Fatalln(err.Error()) } wg.Done() }(clickCount) } wg.Wait() numberResult, err := redisClient.Get(context.Background(), numberKey).Int64() if err != nil { fmt.Println(err.Error()) } fmt.Println(numberResult) } |
输出结果:
1 2 | OK 30256 |
Hash是Redis中存储键值对的数据类型,与String类型不同的是,Hash类型可以存储多个属性和对应的值。Hash类型的特点是支持动态增加属性,单个键值对最多可以存储2^32-1个键值对,可以通过hset、hget等命令进行读写操作。
Hash类型在实际应用中的常见使用场景有:
用户信息存储:存储用户信息,如用户名、密码、邮箱等属性。
商品信息存储:存储商品信息,如商品名称、价格、库存等属性。
数据库缓存:缓存数据库中的数据,如查询结果、数据表等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | func testHash() { // 测试hash hashKey := "UserInfo_id_10086" // 如果key存在,则只是覆盖参数中的key和value addKeyCount, err := redisClient.HSet( context.Background(), hashKey, "user_name", "尼古拉斯-赵四", "user_id", "10086", "like_count", "101", ).Result() if err != nil { log.Fatalln(err.Error()) } fmt.Println("Add", addKeyCount, "new key") addKeyCount, err = redisClient.HSet( context.Background(), hashKey, "height", "183", "birthdate", -25862400000, "user_name", "尼古拉斯-凯奇", ).Result() if err != nil { log.Fatalln(err.Error()) } fmt.Println("Add", addKeyCount, "new key") // 查看更改后的user_name result, err := redisClient.HGet(context.Background(), hashKey, "user_name").Result() if err != nil { log.Fatalln(err.Error()) } fmt.Println(result) // 增加喜欢数key的值 _, err = redisClient.HIncrBy(context.Background(), hashKey, "like_count", 66).Result() if err != nil { log.Fatalln(err.Error()) } // 获取所有的键值对数据 mapResult, err := redisClient.HGetAll(context.Background(), hashKey).Result() if err != nil { log.Fatalln(err.Error()) } fmt.Println(mapResult) // 默认永不过期,如有需求,可手动设置过期时间 succeed, err := redisClient.Expire(context.Background(), hashKey, time.Second*20).Result() if err != nil { log.Fatalln(err.Error()) } fmt.Println(succeed) } |
输出结果:
1 2 3 4 5 | Add 3 new key Add 2 new key 尼古拉斯-凯奇 map[birthdate:-25862400000 height:183 like_count:167 user_id:10086 user_name:尼古拉斯-凯奇] true |
List是Redis中存储有序列表的数据类型,它的特点是可以按照插入顺序存储多个元素,每个元素都有一个索引值。List类型的特点是支持在列表头部或尾部插入或删除元素,可以通过lpush、rpush等命令进行读写操作。
List类型在实际应用中的常见使用场景有:
消息队列:通过lpush/rpop命令实现消息队列,如任务队列、事件队列等。
最新消息存储:存储最新的消息,如最新的微博、动态等。
用户行为记录:记录用户的操作历史,如最近浏览过的商品、最近听的歌曲等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | type ActionMessage struct { MessageID string `json:"message_id"` MessageType string `json:"message_type"` } // 实现 encoding.BinaryMarshaler 接口 func (m *ActionMessage) UnmarshalBinary(data []byte) error { return json.Unmarshal(data, m) } func (m *ActionMessage) MarshalBinary() (data []byte, err error) { return json.Marshal(m) } func testList() { // 测试list listKey := "ActionMessageQueue" go func() { readList(listKey) }() go func() { for i := 0; i < 100; i++ { messageItem := ActionMessage{ MessageID: fmt.Sprintf("%d", i+1), MessageType: fmt.Sprintf("%d", i%5), } _, err := redisClient.LPush(context.Background(), listKey, &messageItem).Result() if err != nil { log.Fatalln(err.Error()) } time.Sleep(time.Millisecond * 100) } }() } func readList(key string) { var messageItem ActionMessage err := redisClient.RPop(context.Background(), key).Scan(&messageItem) if err != nil { // 出现错误,等待500毫秒继续读取 time.Sleep(time.Millisecond * 500) fmt.Println("sleep") readList(key) return } fmt.Println(messageItem) readList(key) } |
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | sleep {1 0} {2 1} {3 2} {4 3} {5 4} sleep {6 0} {7 1} {8 2} {9 3} {10 4} ………… sleep sleep sleep ………… |
Set是Redis中的一种集合结构,它可以存储多个元素,并且每个元素都是唯一的。常用的命令包括SADD、SREM、SISMEMBER、SMEMBERS等。Set类型常用于存储用户的关注列表、标签等场景。
其使用场景还可以扩展到跟多的需要进行数据去重的场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | func testSet() { setKey := "FollowUserIds_user_10086" // 添加重复元素 for i := 0; i < 1000; i++ { count, err := redisClient.SAdd( context.Background(), setKey, i%11, // 仅11个不重复元素,注意这里是int类型 ).Result() if err != nil { log.Fatalln(err.Error()) } else { fmt.Println(count) } } // 继续添加重复元素,注意这里是string类型 count, err := redisClient.SAdd( context.Background(), setKey, "1", "2", "2", "3", "3", "3", "3", "3", "3", "3", "4", "4", "4", ).Result() if err != nil { fmt.Println(err.Error()) } else { fmt.Println(count) } // 添加不重复的元素 count, err = redisClient.SAdd(context.Background(), setKey, "33").Result() if err != nil { fmt.Println(err.Error()) } else { fmt.Println(count) } // 获取不重复元素个数 count, err = redisClient.SCard(context.Background(), setKey).Result() if err != nil { fmt.Println(err.Error()) } else { fmt.Println(count) } // 获取最终结果 results, err := redisClient.SMembers(context.Background(), setKey).Result() if err != nil { fmt.Println(err.Error()) } else { fmt.Println(results) } } |
输出结果:
1 2 | 12 [0 1 2 3 4 5 6 7 8 9 10 33] |
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 – 1 (4294967295, 每个集合可存储40多亿个成员)。
我们可以用Sorted Set实现诸如商品推荐,用户推荐等场景下的数据缓存,而且在仅考虑单个数据推荐指数的情况下不用担心出现数据重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | func testSortedSet() { sortedSetKey := "RecommendUsersKey" // 添加用户ID 1-1000,且赋予一个0-500的随机分数 for i := 0; i < 1000; i++ { count, err := redisClient.ZAdd(context.Background(), sortedSetKey, redis.Z{ Score: randFloats(0, 500.0, 1)[0], Member: i + 1, }).Result() if err != nil { log.Fatalln(err.Error()) } else { fmt.Println(count) } } // 用户1001设置分数为10086,一定为最大 count, err := redisClient.ZAdd(context.Background(), sortedSetKey, redis.Z{ Score: 10086, Member: "1001", }).Result() if err != nil { fmt.Println(err.Error()) } else { fmt.Println(count) } // 获取分数最高的20个用户作为推荐结果 results, err := redisClient.ZRevRange(context.Background(), sortedSetKey, 0, 20).Result() if err != nil { fmt.Println(err.Error()) } else { fmt.Println(results) } // 删除后900名用户 count, err = redisClient.ZRemRangeByRank(context.Background(), sortedSetKey, 0, 899).Result() if err != nil { fmt.Println(err.Error()) } else { fmt.Println(count) } } |
输出结果:
1 2 | [1001 572 159 731 355 667 827 424 953 157 435 743 167 365 524 854 818 881 134 747 773] 900 |
1. String是Redis所有基础类型的基础,其他类型中存储的value最终都是转化为二进制安全的字节数组以string的形式存储。
2. 利用incrby,decrby等命令可以结合Redis的单线程特性方便的进行数据统计。
3. 利用Hash类型可以方便的进行使用key对数据进行读取,从而免去复杂的数据库查询,减少数据库压力。
4. List类型可以很方便的进行列表数据缓存,甚至能实现简单的消息队列。
5. Set和Sorted Set都能方便的对数据进行去重,也可以陈本极低的实现推荐数据存储。
6. 需要注意的是,存储非基础类型的数据是,需要将数据转换为字节数组,需要考虑不同变成语言数据序列化时候的差异。
程序猿老龚(龚杰洪)原创,版权所有,转载请注明出处.