认识redis 和常见异常问题了解

93 阅读7分钟

1.认识redis

redis- 内存数据库,本质就是存储数据。

对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。

1.对比mysql 理解redis.

mysqlredis
存储位置磁盘中内存中
存储方式以表格的形式存储key-value 形式
操作方式使用特定的符合语法的语句使用简单的命令(如 SET、GET、HSET、HGET 等)

2.redis的常用数据类型

Redis 主要支持以下几种数据类型:

  • string(字符串): ****基本的数据存储单元,可以存储字符串、整数或者浮点数。
  • hash(哈希): 一个键值对集合,可以存储多个字段。
  • list(列表): 一个简单的列表,可以存储一系列的字符串元素。
  • set(集合): 一个无序集合,可以存储不重复的字符串元素。
  • zset(sorted set:有序集合): ****类似于集合,但是每个元素都有一个分数(score)与之关联。
  • 位图(Bitmaps): 基于字符串类型,可以对每个位进行操作。
  • 超日志(HyperLogLogs): 用于基数统计,可以估算集合中的唯一元素数量。
  • 地理空间(Geospatial): 用于存储地理位置信息。
  • 发布/订阅(Pub/Sub): 一种消息通信模式,允许客户端订阅消息通道,并接收发布到该通道的消息。
  • 流(Streams): 用于消息队列和日志存储,支持消息的持久化和时间排序。
  • 模块(Modules): Redis 支持动态加载模块,可以扩展 Redis 的功能。

3.官网-redis.io

2.redis 的安装

3.redis 客户端操作和访问

127.0.0.1:6379> SET mykey "a"
OK
127.0.0.1:6379> GET mykey
"a"
127.0.0.1:6379> EXISTS mykey
(integer) 1
127.0.0.1:6379> DEL mykey
(integer) 1
127.0.0.1:6379> EXISTS mykey
(integer) 0
127.0.0.1:6379> PEXPIRE mykey 20000   
(integer) 1
127.0.0.1:6379> setex key2 10 "hello"
OK

4.redis 的使用场景举例

通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。

以登录场景为例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form id="loginForm">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" required>
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" required>
        </div>
        <button type="submit">Login</button>
    </form>
    <div id="message"></div>

    <script>
        document.getElementById('loginForm').addEventListener('submit', async function(event) {
            event.preventDefault();
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;

            const response = await fetch('/login', {
                method: 'POST',
                body: JSON.stringify({ username: username, password: password }),
            });
            const result = await response.json();
            document.getElementById('message').innerText = result.message;
        });
    </script>
</body>
</html>
import json
import redis
from http.server import BaseHTTPRequestHandler, HTTPServer

# 连接到Redis服务器
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True)

# 模拟数据库
users_db = {
    "user1": "password1",
    "user2": "password2"
}

class RequestHandler(BaseHTTPRequestHandler):
    def _set_headers(self, status_code=200):
        self.send_response(status_code)
        self.send_header('Content-type', 'application/json')
        self.end_headers()

    def do_POST(self):
        if self.path == '/login':
            content_length = int(self.headers['Content-Length'])
            post_data = self.rfile.read(content_length)
            data = json.loads(post_data.decode('utf-8'))

            username = data['username']
            password = data['password']

            cached_password = redis_client.get(username)
            if cached_password:
                if cached_password == password:
                    self._set_headers(200)
                    response = {'message': 'Login successful (from cache)'}
                else:
                    self._set_headers(401)
                    response = {'message': 'Invalid credentials (from cache)'}
            else:
                db_password = users_db.get(username)
                if db_password and db_password == password:
                    redis_client.setex(username, 3600, db_password)
                    self._set_headers(200)
                    response = {'message': 'Login successful'}
                else:
                    self._set_headers(401)
                    response = {'message': 'Invalid credentials'}

            self.wfile.write(json.dumps(response).encode('utf-8'))

def run(server_class=HTTPServer, handler_class=RequestHandler, port=8080):
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    print(f'Starting httpd server on port {port}')
    httpd.serve_forever()

if __name__ == '__main__':
    run()

5.缓存异常面临的三大问题

5.1缓存雪崩

缓存雪崩是指缓存中大量的数据在同一时间过期或失效或缓存服务器宕机,导致大量的请求直接访问后端数据库服务器,从而引起数据库服务器负载过高甚至崩溃的现象

发生的场景

1.大量缓存同时过期

2.redis 故障

解决方案

1.均匀设置过期时间

如果要给缓存数据设置过期时间,应该避免将大量数据设置成同一过期时间,我们可以在对缓存数数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就能保证数据不会在同一时间过期

import random
import redis

client = redis.StrictRedis(host='localhost', port=6379, db=0)

# 设置缓存,过期时间加上一个随机的秒数
client.setex('key', 3600 + random.randint(0, 300), 'value')

2.互斥锁

当业务线在处理用户请求时 ,如果发现访问的数据不在redis里,就加一个互斥锁,保证同一时间内同一数据只有一个请求来构建缓存(从数据库读取数据,再将数据更新到redis)当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放重新读取缓存,要么就就返回空值或者默认值

实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

3.缓存预热

在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,比如使用后台线程定时更新缓存

4.快速熔断或请求限流机制

熔断机制:暂停业务应用对缓存服务的访问,直接返回错误,等到redis恢复正常后,再允许业务应用访问缓存服务

请求限流机制:将少量请求发送打数据库进行处理,再多的请求就在入口直接拒绝服务,直到redis恢复正常并把缓存预热完成,在解除请求限流

5.构建redis 缓存高可靠集群

方法4为雪崩后的应对方案,为了避免故障宕机导致的缓存雪崩问题,可以通过主从节点的方式构建redis 集群,如果redis 主节点宕机,从节点会自动切换成主节点,继续提供缓存服务(哨兵机制

5.2缓存击穿

我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,热搜排行榜等,这类被频地访问的数据被称为热点数据。如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题

可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集

解决方案:

1.合理的缓存时间:设置较短的缓存过期时间,并定时更新,对热点数据设置较长的缓存过期时间,比如设置为永不过期

2.互斥锁

5.3缓存穿透

当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

发生的场景

1.业务误操作,缓存和数据库中的数据误删除,导致缓存和数据库中没有数据

2.黑客恶意攻击,大量访问某些不存在的业务

解决方案

1.非法请求的限制

2.设置缓存空值或者默认值:对于查询结果为空的请求,为对应的key设置一个空值或缺省值,这样后续的请求再进行查询直接返回空值或者缺省值,防止频繁访问数据库的情况出现

3.使用布隆过滤器判断数据是否存在,避免通过查询数据库来判断数据是否存在

在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线确认缓存失效,可以通过查询布隆过滤器判断数据是否存在,如果不存在,就不用查询数据库来判断数据是否存在了,也就是说发生缓存穿透时,大量请求只会查询redis 和布隆过滤器,而不查询数据库,但是布隆过滤器会有误判的现象 (具体原理自行查询)