Redis简介
什么是Redis
Redis(Remote Dictionary Server)是一个开源的内存数据库和缓存系统,它提供了高性能、持久性存储、分布式支持以及多种数据结构的支持。Redis 最初由Salvatore Sanfilippo编写,现在由一个活跃的开源社区维护。Redis 常用于解决各种用例,包括缓存、消息队列、会话存储、排行榜、实时分析和计数器等。
以下是 Redis 的一些关键特点和用途:
- 内存存储:Redis 数据存储在内存中,这使得它具有极快的读写访问速度。这使得 Redis 成为高吞吐量应用程序的理想选择,尤其是在需要快速响应的情况下。
- 数据持久性:Redis 支持多种持久性选项,包括快照(Snapshots)和日志(Append-Only Files)。这意味着您可以根据需求配置 Redis,以便在发生故障时保留数据。
- 多种数据结构:Redis 不仅仅是一个键值存储系统,它支持多种数据结构,包括字符串、列表、集合、有序集合、散列等。这些数据结构使 Redis 成为处理各种数据需求的强大工具。
- 发布/订阅模式:Redis 支持发布/订阅模式,使得客户端能够订阅特定频道上的消息。这用于构建实时通信和事件处理系统。
- 事务支持:Redis 支持事务,允许您将多个命令打包到一个事务中,并在一次操作中执行它们。如果某个命令失败,整个事务将回滚,以确保数据一致性。
- 分布式支持:Redis 提供了复制和分片的支持,允许构建具有高可用性和负载均衡的 Redis 集群。这使得 Redis 成为分布式应用程序的核心组件之一。
- 丰富的客户端库:Redis 提供了多种语言的客户端库,包括 Python、Java、Go、Node.js 等,这使得与 Redis 集成到不同类型的应用程序非常容易。
- 社区支持和生态系统:Redis 拥有庞大的开源社区,提供了丰富的文档、教程和扩展。还有许多与 Redis 集成的工具和框架,如 Celery、Django、Ruby on Rails 等。
为什么需要Redis
数据量增长,读写数据压力不断增加。所以需要将热数据(经常被访问到的数据)存储在内存中。
redis的基本工作原理
- 数据从内存中读写
- 数据保存到硬盘上防止重启数据丢失
- 增量数据保存在AOF文件
- 全量数据RDB文件
- 单线程处理所有操作命令
Redis应用案例
连续签到
掘金每日连续签到,业务逻辑:用户每日有一次签到的机会,每天要在23:59:59秒之前签到,如果断签,连续签到计数将归0。使用到了redis的两个能力:在原有的基础上加一以及过期。 addContinuesDays 为用户签到续期
func addContinuesDays(ctx context.Context, userID int64) {
key := fmt.Sprintf(continuesCheckKey, userID)
// 1. 连续签到数+1
err := RedisClient.Incr(ctx, key).Err()
if err != nil {
fmt.Errorf("用户[%d]连续签到失败", userID)
} else {
expAt := beginningOfDay().Add(48 * time.Hour)
// 2. 设置签到记录在后天的0点到期
if err := RedisClient.ExpireAt(ctx, key, expAt).Err(); err != nil {
panic(err)
} else {
// 3. 打印用户续签后的连续签到天数
day, err := getUserCheckInDays(ctx, userID)
if err != nil {
panic(err)
}
fmt.Printf("用户[%d]连续签到:%d(天), 过期时间:%s", userID, day, expAt.Format("2006-01-02 15:04:05"))
}
}
}
获取用户连续签到天数
func getUserCheckInDays(ctx context.Context, userID int64) (int64, error) {
key := fmt.Sprintf(continuesCheckKey, userID)
days, err := RedisClient.Get(ctx, key).Result()
if err != nil {
return 0, err
}
if daysInt, err := strconv.ParseInt(days, 10, 64); err != nil {
panic(err)
} else {
return daysInt, nil
}
}
消息通知
用list作为消息队列,是由一个双向链表和Listpack实现的。listpack是在一个链表节点上面压缩了很多数据,基本思路是开辟了一个大的内存空间,有一个开始byte和一个结束byte以及元素长度,每个元素的长度是相同的,就能从中切出来。
限流
一方面是防止网站被攻击,一方面是服务器流量有限。就要求1秒钟放行的请求为N,超过N则禁止访问。
if currentQPS > ex03MaxQPS {
// 超过流量限制,请求被限制
eventLogger.Append(common.EventLog{
EventTime: time.Now(),
Log: common.LogFormat(routine, "被限流[%d]", currentQPS),
})
// sleep 模拟业务逻辑耗时
time.Sleep(50 * time.Millisecond)
err = RedisClient.Decr(ctx, key).Err()
if err != nil {
panic(err)
}
} else {
// 流量放行
eventLogger.Append(common.EventLog{
EventTime: time.Now(),
Log: common.LogFormat(routine, "流量放行[%d]", currentQPS),
})
atomic.AddInt32(&accessQueryNum, 1)
time.Sleep(20 * time.Millisecond)
}
Redis个人实践
用Redis实现一个评论发表和查看功能
需求分析:
- 发布一条评论 √
- 获取文章的所有评论 √ 相较于关系型数据库的优势在于:
- 快速读写访问:Redis 是一个基于内存的数据库,具有出色的读写性能。对于实时评论、点赞、阅读计数等需要快速响应的操作,Redis 提供了比传统磁盘存储的 MySQL 更快的响应时间。
- 实时更新:Redis 支持发布/订阅模式,这意味着评论和点赞等操作可以实时地传播给所有关注此信息的用户。这对于实时社交互动非常重要。
- 缓存:Redis 可以用作缓存层,可以缓存常用的评论和数据,以减轻数据库服务器的负载。这可以提高整体性能。
- 简单的数据结构:对于评论数据,通常只需要键值对来存储。Redis 提供了简单的数据结构,如字符串和哈希表,用于存储评论数据,这比关系型数据库的表结构更简单。
- 横向扩展:Redis 支持分布式部署,可以轻松地在需要时扩展到多个节点,以应对高负载情况。这使得系统更容易扩展,而不需要进行复杂的数据库集群设置。
- 实时性能监控:Redis 提供了丰富的性能监控工具,可以实时跟踪 Redis 服务器的性能,以便进行优化和调整。
代码如下
var ctx = context.Background()
func main() {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
defer client.Close()
comment := Comment{
ID: "1",
UserID: "user123",
Content: "这是一条评论。",
Created: time.Now().Unix(),
}
err := PublishComment(client, comment)
if err != nil {
fmt.Println("评论发布失败:", err)
return
}
fmt.Println("评论发布成功!")
articleID := "article123"
comments, err := GetComments(client, articleID)
if err != nil {
fmt.Println("获取评论失败:", err)
return
}
fmt.Println("文章的所有评论:")
for _, c := range comments {
fmt.Printf("用户:%s,评论:%s\n", c.UserID, c.Content)
}
}
type Comment struct {
ID string
UserID string
Content string
Created int64
}
func PublishComment(client *redis.Client, comment Comment) error {
key := fmt.Sprintf("comments:%s", comment.ID)
data := map[string]interface{}{
"userID": comment.UserID,
"content": comment.Content,
"created": comment.Created,
}
err := client.HMSet(ctx, key, data).Err()
if err != nil {
return err
}
articleKey := fmt.Sprintf("article:%s:comments", comment.ID)
err = client.ZAdd(ctx, &redis.Z{
Score: float64(comment.Created),
Member: key,
}, articleKey).Err()
return err
}
func GetComments(client *redis.Client, articleID string) ([]Comment, error) {
articleKey := fmt.Sprintf("article:%s:comments", articleID)
members, err := client.ZRange(ctx, articleKey, 0, -1).Result()
if err != nil {
return nil, err
}
comments := make([]Comment, 0, len(members))
for _, member := range members {
commentData, err := client.HGetAll(ctx, member).Result()
if err != nil {
return nil, err
}
comment := Comment{
ID: member,
UserID: commentData["userID"],
Content: commentData["content"],
Created: time.Now().Unix(),
}
comments = append(comments, comment)
}
return comments, nil
}