MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

缓存与CDN的联合应用与性能提升

2024-10-317.9k 阅读

缓存与 CDN 的基础概念

缓存的定义与工作原理

缓存是一种临时数据存储机制,旨在提高数据访问速度。在后端开发中,当应用程序需要获取数据时,首先会检查缓存中是否存在所需数据。如果存在(即缓存命中),则直接从缓存中获取数据,而无需访问原始数据源,如数据库。这大大减少了数据获取的时间,提高了应用程序的响应速度。

例如,一个新闻网站可能会缓存热门文章的内容。当用户请求查看这些热门文章时,系统先检查缓存。若缓存中有该文章内容,直接返回给用户,避免了从数据库中查询文章的复杂操作。

缓存的工作原理基于“存储 - 查找 - 命中/未命中”的流程。数据在首次被请求并从数据源获取后,会被存储到缓存中,同时会关联一个唯一的键(Key)。后续请求时,通过这个键在缓存中查找数据。如果找到,就是缓存命中;若未找到,则是缓存未命中,需要从数据源获取数据并更新缓存。

CDN 的定义与工作原理

CDN(Content Delivery Network)即内容分发网络,它是一个分布式服务器网络,旨在通过将内容缓存到离用户更近的服务器,来提高内容的分发速度。CDN 的工作原理是基于 DNS 重定向和缓存技术。

当用户请求访问一个网站的资源(如图片、脚本、样式表等)时,用户的请求首先到达本地 DNS 服务器。本地 DNS 服务器会查询 CDN 的全局负载均衡系统(GSLB)。GSLB 根据用户的地理位置、服务器负载等因素,返回距离用户最近且负载较低的 CDN 节点服务器地址。用户的请求就会被重定向到该 CDN 节点服务器。

如果该 CDN 节点服务器缓存中有用户请求的资源,就直接返回给用户;如果没有,则该 CDN 节点服务器会向源服务器请求资源,获取后缓存起来并返回给用户。例如,一个全球知名的视频网站,其视频内容通过 CDN 分发到世界各地的节点服务器。当欧洲的用户请求观看视频时,CDN 会将其请求导向欧洲的 CDN 节点,快速提供视频内容,避免了从位于美国的源服务器获取数据可能带来的高延迟。

缓存与 CDN 的联合应用场景

网站静态资源加速

在现代网站开发中,大量的静态资源,如 CSS、JavaScript 文件、图片等,需要快速加载以提供良好的用户体验。通过将这些静态资源同时部署到 CDN 和后端缓存中,可以实现高效的加载。

CDN 负责将静态资源缓存到离用户近的节点,快速响应前端请求。而后端缓存则可以在服务器端缓存这些资源,减少对存储系统(如文件系统或对象存储)的重复读取。例如,一个电商网站的商品图片,既可以通过 CDN 快速展示给用户,后端服务器在处理相关业务逻辑时,若需要再次获取图片信息(如进行图片尺寸调整等操作),可以从后端缓存中获取,避免了从存储中重复读取。

下面是一个简单的 Python 代码示例,展示如何在后端使用缓存来管理静态资源路径信息:

import hashlib
from flask import Flask, send_file
import cachetools

app = Flask(__name__)
cache = cachetools.TTLCache(maxsize=100, ttl=3600)


def get_static_resource_path(resource_name):
    key = hashlib.md5(resource_name.encode()).hexdigest()
    if key in cache:
        return cache[key]
    # 假设这里是从配置文件或数据库获取静态资源实际路径的逻辑
    real_path = f"/var/www/static/{resource_name}"
    cache[key] = real_path
    return real_path


@app.route('/static/<resource_name>')
def serve_static(resource_name):
    path = get_static_resource_path(resource_name)
    return send_file(path)


if __name__ == '__main__':
    app.run()

动态内容加速

虽然 CDN 传统上主要用于静态内容分发,但通过与后端缓存联合,可以实现动态内容的加速。例如,对于一些不经常变化的动态页面,如新闻文章页面、产品介绍页面等,可以在后端缓存生成的页面内容,并通过 CDN 进行分发。

后端在生成动态页面后,将其缓存起来,并设置合适的缓存过期时间。当有用户请求该页面时,若缓存未过期,后端直接返回缓存的页面内容。同时,CDN 也可以缓存这些页面,根据用户的地理位置快速提供服务。这样,既减少了后端服务器的处理压力,又提高了用户获取内容的速度。

以下是一个使用 PHP 和 Memcached 实现动态页面缓存的示例代码:

<?php
$memcache = new Memcached();
$memcache->addServer('localhost', 11211);

$page_id = $_GET['page_id'];
$cache_key = 'dynamic_page_'. $page_id;

if ($memcache->get($cache_key)) {
    echo $memcache->get($cache_key);
} else {
    // 这里假设是生成动态页面的逻辑,例如从数据库查询数据并渲染页面
    $page_content = generateDynamicPage($page_id);
    $memcache->set($cache_key, $page_content, 3600); // 缓存 1 小时
    echo $page_content;
}

function generateDynamicPage($page_id) {
    // 实际逻辑:从数据库查询数据,根据数据渲染页面
    return "This is dynamic page content for page ID $page_id";
}
?>

减轻源服务器负载

联合应用缓存与 CDN 可以显著减轻源服务器的负载。CDN 承担了大量用户请求的响应工作,将频繁访问的内容缓存并直接提供给用户,减少了源服务器的请求量。而后端缓存则进一步在服务器内部减少对数据源(如数据库、文件系统等)的访问。

以一个高流量的在线论坛为例,大量用户频繁访问热门帖子。CDN 缓存帖子的静态部分(如帖子内容的 HTML 渲染、相关图片等),直接响应给用户。后端缓存则缓存帖子的数据库查询结果(如帖子的回复数量、点赞数等动态数据),减少数据库的查询压力。这样,源服务器可以将更多资源用于处理核心业务逻辑,如用户认证、新帖子发布等操作。

缓存与 CDN 联合应用的性能提升策略

合理设置缓存过期时间

缓存过期时间的设置至关重要。如果过期时间设置过长,可能导致数据更新不及时,用户获取到的是旧数据;若设置过短,则会频繁出现缓存未命中,降低缓存的效率。

对于 CDN 缓存,对于静态资源(如样式表、脚本等),可以设置较长的过期时间,因为这些资源通常更新频率较低。例如,将 CSS 文件的 CDN 缓存过期时间设置为一年。而后端缓存对于动态数据,需要根据数据的变化频率来设置过期时间。比如,电商网站的商品库存信息变化较为频繁,后端缓存过期时间可以设置为几分钟;而商品的基本描述信息变化较少,过期时间可以设置为几小时甚至一天。

以下是一个在 Java 中使用 Ehcache 设置缓存过期时间的示例:

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    <defaultCache
            maxEntriesLocalHeap="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskSpoolBufferSizeMB="30"
            maxEntriesLocalDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"/>
    <cache
            name="productDescriptionCache"
            maxEntriesLocalHeap="500"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="3600"
            diskSpoolBufferSizeMB="30"
            maxEntriesLocalDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"/>
    <cache
            name="productStockCache"
            maxEntriesLocalHeap="200"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            diskSpoolBufferSizeMB="30"
            maxEntriesLocalDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"/>
</ehcache>

在上述配置中,productDescriptionCache 缓存商品描述信息,设置了 1 小时的过期时间;productStockCache 缓存商品库存信息,设置了 2 分钟的过期时间。

缓存预热

缓存预热是指在系统正式运行前,提前将一些常用数据加载到缓存中。对于 CDN,可以通过主动推送或预取的方式,将热门内容提前缓存到各个节点。而后端缓存也可以在服务器启动时,加载一些初始化数据到缓存中。

例如,一个在线教育平台在每日凌晨系统访问量较低时,通过脚本将当天预计会被大量访问的课程介绍页面、视频资源等推送到 CDN 节点,并在后端缓存中加载课程的基本信息(如课程名称、讲师信息等)。这样,当白天用户大量访问时,缓存命中率会大大提高,系统响应速度更快。

以下是一个使用 Redis 进行缓存预热的 Python 代码示例:

import redis
import json

r = redis.Redis(host='localhost', port=6379, db=0)

# 假设这里从数据库获取热门课程信息
def get_popular_courses_from_db():
    # 实际实现:连接数据库,执行查询语句,返回课程信息列表
    courses = [
        {'id': 1, 'name': 'Python Programming Basics', 'instructor': 'John Doe'},
        {'id': 2, 'name': 'Advanced Java Development', 'instructor': 'Jane Smith'}
    ]
    return courses


courses = get_popular_courses_from_db()
for course in courses:
    key = f'course:{course["id"]}'
    r.set(key, json.dumps(course))

缓存分层策略

采用缓存分层策略可以进一步提高缓存与 CDN 联合应用的性能。可以将缓存分为多级,如浏览器缓存、CDN 缓存、后端应用层缓存、数据库缓存等。

浏览器缓存是用户本地的缓存,对于一些静态资源(如图片、样式表等),浏览器会根据设置进行缓存。CDN 缓存负责在网络边缘缓存内容,快速响应区域内用户的请求。后端应用层缓存则在服务器内部缓存应用相关的数据,如业务逻辑计算结果、数据库查询结果等。数据库缓存则是数据库自身提供的缓存机制,减少磁盘 I/O 操作。

以一个社交网络应用为例,用户的头像图片可以在浏览器中缓存较长时间,减少重复下载。CDN 缓存用户发布的图片、视频等内容,快速提供给其他用户。后端应用层缓存用户的好友列表、动态信息等,减少数据库查询。数据库缓存则加速对用户数据的查询操作。通过这种分层缓存策略,可以在不同层次上提高系统性能,减少响应时间。

缓存与 CDN 联合应用中的问题与解决方法

缓存一致性问题

缓存一致性是指缓存数据与源数据保持一致。在缓存与 CDN 联合应用中,由于数据可能在多个地方缓存,当源数据发生变化时,需要及时更新所有相关缓存,否则会导致用户获取到不一致的数据。

解决缓存一致性问题的方法之一是采用缓存失效机制。当源数据更新时,通过消息队列、发布 - 订阅系统等方式通知相关缓存和 CDN 节点,使其失效。例如,在一个电商系统中,当商品价格发生变化时,系统通过消息队列发送通知,后端缓存和 CDN 节点接收到通知后,将对应的商品缓存数据失效,下次用户请求时,就会获取到最新的数据。

以下是一个使用 RabbitMQ 实现缓存失效通知的简单示例:

import pika
import json

# 连接 RabbitMQ 服务器
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# 声明队列
channel.queue_declare(queue='cache_invalidation')

def send_cache_invalidation_message(product_id):
    message = json.dumps({'product_id': product_id, 'action': 'invalidate'})
    channel.basic_publish(exchange='',
                          routing_key='cache_invalidation',
                          body=message)
    print(f"Sent cache invalidation message for product {product_id}")

# 假设商品 ID 为 123 的价格发生变化,发送缓存失效通知
send_cache_invalidation_message(123)

connection.close()

CDN 节点故障处理

CDN 节点可能会因为硬件故障、网络问题等原因出现故障。当某个 CDN 节点故障时,需要确保用户请求能够快速重定向到其他正常的节点。

CDN 提供商通常会采用冗余设计和负载均衡技术来解决这个问题。通过多个 CDN 节点的冗余部署,当一个节点出现故障时,GSLB 可以将用户请求重定向到其他健康的节点。同时,CDN 节点之间也会进行数据同步和备份,以确保故障节点恢复后能够快速恢复服务。

对于后端应用,也可以通过设置备用 CDN 地址或采用重试机制来应对 CDN 节点故障。例如,在前端代码中,可以设置多个 CDN 地址,当第一个 CDN 地址请求失败时,尝试从第二个 CDN 地址获取资源。在后端代码中,可以使用重试库,如 Python 的 tenacity 库,在请求 CDN 资源失败时进行重试。

from tenacity import retry, stop_after_attempt, wait_fixed

@retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
def get_cdn_resource(url):
    import requests
    response = requests.get(url)
    if response.status_code!= 200:
        raise Exception(f"Failed to get resource from CDN: {response.status_code}")
    return response.content


cdn_url = "https://cdn.example.com/image.jpg"
try:
    resource = get_cdn_resource(cdn_url)
    # 处理获取到的资源
except Exception as e:
    print(f"Failed to get CDN resource after retries: {e}")

缓存穿透问题

缓存穿透是指查询一个不存在的数据,由于缓存中没有,每次都会查询到数据源,导致大量无效请求穿透缓存到达数据源,可能使数据源压力过大甚至崩溃。

解决缓存穿透问题的方法之一是布隆过滤器(Bloom Filter)。布隆过滤器是一种概率型数据结构,可以快速判断一个元素是否存在于集合中。在后端缓存应用中,当数据写入数据源时,同时将数据的键值添加到布隆过滤器中。当查询数据时,先通过布隆过滤器判断键值是否可能存在,如果不存在,则直接返回,避免查询数据源。

以下是一个使用 Python 的 pybloomfiltermmap 库实现布隆过滤器解决缓存穿透问题的示例:

from pybloomfiltermmap import BloomFilter

# 创建布隆过滤器,预计元素数量为 10000,误判率为 0.01
bloom = BloomFilter(capacity=10000, error_rate=0.01, filename='bloomfilter.bf')

# 假设数据写入数据库时,同时添加到布隆过滤器
def add_data_to_db_and_bloom(data_id):
    # 实际逻辑:将数据写入数据库
    print(f"Added data {data_id} to database")
    bloom.add(str(data_id))


# 查询数据时,先通过布隆过滤器判断
def get_data(data_id):
    if str(data_id) not in bloom:
        print(f"Data {data_id} does not exist, no need to query database")
        return None
    # 实际逻辑:查询数据库获取数据
    print(f"Querying database for data {data_id}")
    return f"Data {data_id} from database"


# 示例操作
add_data_to_db_and_bloom(123)
result = get_data(123)
print(result)
result = get_data(456)
print(result)

通过以上对缓存与 CDN 联合应用的各个方面的深入探讨,包括基础概念、应用场景、性能提升策略以及常见问题的解决方法,希望能帮助后端开发人员更好地设计和实现高效的缓存与 CDN 联合架构,提升系统的性能和用户体验。