使用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概念
- 非关系型的键值对数据库,可以根据键以O(1)的时间复杂度取出或插入关联值
- Reds的数据是存在内存中的
- 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的
- 键值对中的值类型可以是 string,hash,list,set, sorted set等
- Reds内置了复制,磁盘持久化,LUA脚本,事务,SSL,客户端代理等功能
- 通过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秒过期。单位秒部分就是随机时间,这样过期时间就分散了。 对于热点数据,过期时间尽量设置得长一些,冷门的数据可以相对设置过期时间短一些。
-
使用缓存集群,避免单机宕机造成的缓存雪崩。