使用Redis来缓存数据第二课 |青训营

288 阅读7分钟

使用Redis来缓存数据第二课 |青训营

为什么要使用redis?

Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,也被称为键值存储数据库。它支持多种数据结构,例如字符串(String)、列表(List)、哈希(Hash)、集合(Set)和有序集合(Sorted Set)等。与其他键值存储数据库不同的是,Redis将数据保存在内存中,因此具有非常高的读写性能。同时,它还可以将数据持久化到磁盘上,实现数据的持久化存储。 Redis主要用于解决高并发场景下的数据缓存和读写性能问题。它常用于构建实时应用、缓存、消息队列、计数器、排行榜等场景。其特点包括高速的读写性能、支持丰富的数据结构、原子性操作和丰富的扩展功能等。 除了基本的数据结构操作,Redis还提供了一些高级功能,如发布订阅(Pub/Sub)、事务(Transaction)、事件通知和Lua脚本等。它还具有集群和分片功能,可以将数据分布在多个节点上,实现高可用和扩展性。

总之,Redis是一个灵活、高性能的键值存储数据库,适用于各种场景的数据缓存和存储需求。

Redis速度

通常情况下,Redis的单实例能够处理数万到数十万的QPS。但这只是一个粗略的估计,实际的QPS可能会受到其他因素的限制。例如,如果Redis服务器的网络连接较慢或者使用了较为复杂的数据结构和操作,QPS可能会有所降低。 一般来说,MySQL的单实例可以处理几千到几万的QPS。但这只是一个粗略的估计,具体的QPS还取决于数据库服务器的性能以及数据库的优化情况。 所以,我们才需要使用Redis来缓存数据库数据

Redis 除了做缓存,还能做什么?

  • 分布式锁 : 通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。

  • 限流 :一般是通过 Redis + Lua 脚本的方式来实现限流

  • 消息队列 :Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。

  • 复杂业务场景 :通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。

redis概念

  1. 非关系型的键值对数据库,可以根据键以O(1)的时间复杂度取出或插入关联值
  2. Reds的数据是存在内存中的
  3. 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的
  4. 键值对中的值类型可以是 string,hash,list,set, sorted set等
  5. Reds内置了复制,磁盘持久化,LUA脚本,事务,SSL,客户端代理等功能
  6. 通过Reds哨兵和自动分区提供高可用性
  • 计数器 可以对Sng进行自增自减运算,从而实现计数器功能。Reds这种内存型数据库的读写性能非常高, 很适合存储频繁读写的计数量

  • 分布式D生成 利用自增特性,一次请求一个大一点的步长如incr2000,缓存在本地使用,用完再请求。

  • 海量数据统计 位图(btmp):存储是否参过次活动,是答已读谋篇文章,用户是否为会员,日活统计。

  • 会话缓存 可以使用 Redis来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就 不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

  • 分布式队列/阻塞队列 List是一个双向链表,可以通过 push/push和rpop/pop写入和读取消息。可以通过使用 brpop/b|pop 来实现阻塞队列

要使用Redis,首先就是要配置Redis的连接信息。为了让性能最大化,使用连接池方式连接Redis。

然后我们尝试使用Redis的增删改查。

func main() {
    RedisInit()
    defer RedisClose()

    var err error
    _, err = rds.Do("SET", "test", "abc") // Redis SET 命令
    if err != nil {
       fmt.Println(err)
       return
    }

    res, err := redis.String(rds.Do("GET", "test")) // Redis GET 命令
    if err != nil {
       fmt.Println(err)
       return
    }
    fmt.Println("res:", res)

    res1, err := redis.String(rds.Do("GET", "test")) // Redis GET 命令
    if err != nil {
       fmt.Println(err)
       return
    }
    fmt.Println("res1:", res1)

    res2, err := redis.Int(rds.Do("GET", "test")) // Redis GET 命令
    if err != nil {
       fmt.Println(err)
       return
    }
    fmt.Println("res2:", res2)

    _, err = rds.Do("DEL", "test") // Redis DEL 命令,删除键 "test"
    if err != nil {
       fmt.Println(err)
       return
    }
    fmt.Println("删除成功")
}

上面是go最简单的增删改查方法。 可以对比java

@RestController  
public class RedisController {  
  
@Resource  
private RedisTemplate<String, Object> redisTemplate;  
  
@GetMapping("/redis")  
public Object handleRedisOperations(@RequestParam(required = false) String key,  
@RequestParam(required = false) String value,  
@RequestParam(required = false) String operation) {  
if (operation == null) {  
// 没有指定操作类型,默认获取所有数据  
List<String> allData = new ArrayList<>();  
Set<String> keys = redisTemplate.keys("*");  
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();  
  
for (String k : keys) {  
Object v = valueOperations.get(k);  
allData.add(k + ": " + v.toString());  
}  
  
return allData;  
} else {  
// 根据指定的操作类型执行相应的操作  
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();  
  
switch (operation) {  
case "get":  
Object v = valueOperations.get(key);  
return v != null ? v.toString() : "Key not found";  
case "set":  
valueOperations.set(key, value);  
return "Data added successfully";  
case "update":  
if (valueOperations.get(key) != null) {  
valueOperations.set(key, value);  
return "Data updated successfully";  
} else {  
return "Key not found";  
}  
case "delete":  
Boolean isDeleted = redisTemplate.delete(key);  
return isDeleted ? "Data deleted successfully" : "Key not found";  
default:  
return "Invalid operation";  
}  
}  
}  
}

两个语言都是挺简单的。 然后就是使用Redis来缓存数据库 思路:当redis里面没有要访问的数据时候。才从数据库中获取,并且放到Redis中。设置过期时间,并且当查询的值为的时候,缓存一个空的值。而不是去数据库中查询。这样能有效的防止缓存穿透。

下面是简单的例子:

配置Redis信息
var rdb = redis.NewClient(&redis.Options{
    Addr:     "192.168.3.190:6379", // Redis服务器地址
    Password: "",                   // Redis数据库密码
    DB:       0,                    // 选择Redis数据库
})
list := []*Response.VideoList{}

// 首先尝试从Redis中获取缓存数据
cacheKey := "feed_video"
cacheResult, err := rdb.Get(cacheKey).Result()
if err == nil {
    // 缓存数据存在,直接返回
    err = json.Unmarshal([]byte(cacheResult), &list)
    if err == nil {
       return list
    }
}
//没有缓存就从数据库中获取
......省略获取数据库数据....


// 将数据库的结果存入Redis缓存
cacheValue, err := json.Marshal(list)
if err == nil {
    rdb.Set(cacheKey, cacheValue, 1*time.Minute) // 设置缓存过期时间,这里设置为1分钟
   
}

return list

对于一些存活时间可以比较长的数据可以设置久一点。例如视频信息,用户的账号密码以及用户信息等。 对于一些数据,例如实时金额,实时性的数据,缓存信息设置可以短一点。

缓存穿透、缓存雪崩

  • 缓存穿透:热点数据查询绕过缓存,直接查询数据库
  • 缓存雪崩:大量缓存同时过期

缓存穿透的危害

  • 查询一个一定不存在的数据 通常不会缓存不存在的数据,这类查询请求都会直接打到db,如果有系统bug或人为攻击,那么容易导致db响应慢甚至宕机
  • 缓存过期时 批量缓存过期导致缓存穿透 在高并发场景下,一个热key如果过期,会有大量请求同时击穿至db,容易影响db性能和稳定。 同一时间有大量key集中过期时,也会导致大量请求落到db上,导致查询变慢,甚至出现db无法响应新的查询

如何减少缓存穿透

  • 缓存空值 如一个不存在的userlD。这个id在缓存和数据库中都不存在。则可以缓存一个空值,下次再查缓存直接反空值。
  • 布隆过滤器 通过bloom filter算法来存储合法Key,得益于该算法超高的压缩率,只需占用极小的空间就能存储大量key值。对于请求,先访问过滤器查询key是否存在

如何避免缓存雪崩

  • 缓存空值 将缓存失效时间分散开,比如在原有的失效时间基础上增加一个随机值,例如不同Key过期时间,可以设置为10分1秒过期,10分23秒过期,10分8秒过期。单位秒部分就是随机时间,这样过期时间就分散了。 对于热点数据,过期时间尽量设置得长一些,冷门的数据可以相对设置过期时间短一些。

  • 使用缓存集群,避免单机宕机造成的缓存雪崩。