2024年2月

本文分享自华为云社区《
从HTML到实战:深入解析BeautifulSoup4的爬虫奇妙世界
》,作者:柠檬味拥抱。

网络上的信息浩如烟海,而爬虫技术正是帮助我们从中获取有用信息的重要工具。在爬虫过程中,解析HTML页面是一个关键步骤,而BeautifulSoup4正是一款功能强大的解析器,能够轻松解析HTML和XML文档。本文将介绍BeautifulSoup4的基础知识,并通过实际代码示例进行演示。

BeautifulSoup4简介:

BeautifulSoup4是Python中一个用于解析HTML和XML文档的库,它提供了许多便捷的方法来浏览、搜索和修改文档树。BeautifulSoup4支持多种解析器,其中最常用的是基于Python标准库的html.parser。

安装BeautifulSoup4:

pip install beautifulsoup4

基础知识

解析HTML文档:使用BeautifulSoup4解析HTML文档非常简单,只需要将HTML文档传递给
BeautifulSoup
类即可。

frombs4 import BeautifulSoup

html_doc
= "<html><head><title>My Title</title></head><body><p>Hello, BeautifulSoup4!</p></body></html>"soup= BeautifulSoup(html_doc, 'html.parser')

标签选择器:Beautiful Soup提供了多种标签选择器,最常用的是通过标签名来选择。

# 选择所有的段落标签
paragraphs
= soup.find_all('p')

标签属性:通过指定标签的属性来选择元素。

# 选择class为'example'的div标签
example_div
= soup.find('div', class_='example')

代码实战:

接下来,我们将通过一个简单的实例演示BeautifulSoup4的使用,从一个网页中提取标题和链接。

import requestsfrombs4 import BeautifulSoup

# 发送HTTP请求获取页面内容
url
= 'https://example.com'response= requests.get(url)
html_content
=response.text

# 使用BeautifulSoup解析HTML
soup
= BeautifulSoup(html_content, 'html.parser')

# 提取标题和链接
title
=soup.title.text
links
= soup.find_all('a')

# 打印结果
print(f
"页面标题:{title}")

print(
"页面链接:")for link inlinks:
print(f
"{link.get('href')}")

这个例子中,我们首先发送HTTP请求获取网页内容,然后使用BeautifulSoup解析HTML。通过
soup.title
可以获取页面的标题,通过
soup.find_all('a')
可以获取所有链接标签。最后,我们打印标题和所有链接的地址。

BeautifulSoup4是一个功能强大而灵活的HTML解析库,使得在爬虫项目中处理HTML文档变得更加轻松。通过学习基础知识和实际代码实例,我们可以更好地利用BeautifulSoup4从网页中提取所需信息。在实际项目中,合理运用BeautifulSoup4能够大大提高爬虫的效率和灵活性。

数据处理与异常处理

在实际虫项目中,对于从网页中提取的数据,通常需要进行进一步的处理。BeautifulSoup提供了一些方法来处理提取的文本数据,如去除空白字符、提取数字等。同时,在进行页面解析时,考虑到网页结构可能变化或者异常情况的发生,我们也需要添加适当的异常处理机制。

# 数据处理与异常处理示例for link inlinks:try:
# 提取链接文本并去除首尾空白字符
link_text
=link.text.strip()

# 提取链接地址
link_url
= link.get('href')

# 打印处理后的结果
print(f
"链接文本:{link_text}, 链接地址:{link_url}")

# 进一步处理数据,比如提取数字
iflink_text.isdigit():
number
= int(link_text)
print(f
"提取到数字:{number}")

except Exception
ase:
# 异常处理,打印异常信息
print(f
"处理链接时发生异常:{e}")

在这个例子中,我们首先使用
strip()
方法去除链接文本的首尾空白字符,然后通过
isdigit()
方法判断是否为数字,并进行相应的处理。同时,通过异常处理机制,我们能够捕获并打印在处理链接时可能发生的异常。

高级功能与定制化

BeautifulSoup4还提供了一些高级功能,如CSS选择器、正则表达式等,使得页面解析更加灵活。此外,我们还可以通过定制化解析器、过滤器等方式,满足不同场景下的需求。

# 使用CSS选择器提取数据
main_content
= soup.select_one('#main-content').text

# 使用正则表达式匹配特定模式的数据
import re
pattern
= re.compile(r'\b\d{3}-\d{2}-\d{4}\b')
matches
= soup.find_all(text=pattern)

# 定制化解析器示例
frombs4 import SoupStrainer

only_a_tags
= SoupStrainer("a")
custom_soup
= BeautifulSoup(html_content, 'html.parser', parse_only=only_a_tags)

在这个例子中,我们通过
select_one()
方法使用CSS选择器提取id为
main-content
的元素的文本内容,通过正则表达式匹配特定模式的文本数据,以及通过
SoupStrainer
定制化解析器,只解析
<a>
标签的内容。

遵循爬虫道德准则

在进行网络爬虫的过程中,我们需要遵循一定的爬虫道德准则,以确保爬虫行为的合法性和对被爬取网站的尊重。以下是一些爬虫道德准则:

  1. 尊重网站的
    robots.txt
    文件: 网站的
    robots.txt
    文件通常包含了该站点对爬虫的访问规则,爬虫应该遵守这些规则。通过检查
    robots.txt
    文件,可以了解哪些页面可以被爬取,哪些不可以。

  2. 设置适当的爬虫速率: 爬虫在请求网页时应该遵循适当的速率,以避免对服务器造成过大的负担。设置合理的爬虫速率有助于维护和改善爬虫的可持续性。

  3. 标识爬虫身份: 在HTTP请求的Header中包含爬虫的身份信息,例如User-Agent,以便网站管理员能够识别爬虫并联系到负责人。这有助于建立信任关系。

  4. 避免对服务器造成过大压力: 合理设计爬虫策略,避免在短时间内发送大量请求,以免对目标服务器造成不必要的负担,有可能导致被封禁。

安全注意事项

在爬虫项目中,安全性是一个重要的考虑因素。以下是一些安全注意事项:

  1. 防范反爬虫机制: 有些网站可能会设置反爬虫机制,如验证码、IP封锁等。爬虫应该考虑这些机制,并进行相应的处理,以确保正常的爬取行为。

  2. 处理异常情况: 在爬虫过程中,可能会遇到网络异常、页面结构变化等情况。合理设置异常处理机制,记录日志,以便及时发现和解决问题。

  3. 遵循法律法规: 在进行爬虫活动时,务必遵循相关的法律法规,尊重他人的合法权益。不得进行恶意爬取、盗取信息等违法行为。

实用技巧与优化建议

在进行爬虫开发时,除了掌握基础知识和遵循道德准则外,一些实用技巧和优化建议也能提高爬虫效率和可维护性。

使用Session保持会话: 在爬虫过程中,通过使用
requests.Session
可以保持一个会话,复用TCP连接,提高请求效率,并在多次请求之间保持一些状态信息,如登录状态。

import requests

# 创建Session对象
session
=requests.Session()

# 使用Session发送请求
response
= session.get('https://example.com')

避免频繁请求相同页面: 对于相同的页面,可以考虑缓存已经获取的页面内容,以减轻服务器负担,并提高爬虫的效率。

使用多线程或异步请求: 在大规模爬取数据时,考虑使用多线程或异步请求,以加速数据获取过程。但要注意线程安全性和对目标网站的负载。

定时任务与调度: 对于长时间运行的爬虫任务,考虑使用定时任务和调度工具,确保爬虫按计划执行,同时避免对目标服务器造成不必要的压力。

日志记录: 在爬虫项目中加入合适的日志记录,记录关键信息和异常情况,有助于排查问题和监控爬虫运行状态。

随机化请求头和IP代理: 通过随机化请求头和使用IP代理,可以减小被识别为爬虫的概率,同时提高爬虫的稳定性。

import fake_useragentfrombs4 import BeautifulSoup
import requests

# 随机生成User
-Agent
headers
= {'User-Agent': fake_useragent.UserAgent().random}

# 使用IP代理
proxies
= {'http': 'http://your_proxy', 'https': 'https://your_proxy'}

response
= requests.get('https://example.com', headers=headers, proxies=proxies)

模拟浏览器行为: 有些网站通过检测爬虫的请求头信息来进行反爬虫,此时可以模拟浏览器行为,使请求更接近正常用户的行为。

fromselenium import webdriver

# 使用Selenium模拟浏览器
driver
=webdriver.Chrome()
driver.
get('https://example.com')

通过结合这些实用技巧和优化建议,可以使爬虫更加高效、稳定,同时降低被识别为爬虫的概率。

不断学习与更新

由于网络环境和网站结构的不断变化,爬虫技术也需要不断学习和更新。关注网络爬虫领域的最新发展,学习新的工具和技术,不仅有助于解决新问题,还能提高爬虫项目的适应性和可维护性。

在学习过程中,建议参与相关技术社区、论坛,与其他爬虫开发者交流经验,分享问题和解决方案。这样可以更全面地了解爬虫领域的最新趋势和实践经验,从而更好地提升自己的技能水平。

案例实战:使用BeautifulSoup4爬取新闻信息

让我们通过一个实际案例,使用BeautifulSoup4爬取一个新闻网站的信息。这个案例将演示如何从网页中提取新闻标题、链接和发布时间等信息。

import requestsfrombs4 import BeautifulSoupfromdatetime import datetime

# 发送HTTP请求获取新闻页面内容
url
= 'https://example-news-website.com'response= requests.get(url)
html_content
=response.text

# 使用BeautifulSoup解析HTML
soup
= BeautifulSoup(html_content, 'html.parser')

# 提取新闻信息
news_list
=[]for news_item in soup.find_all('div', class_='news-item'):try:
# 提取新闻标题
title
= news_item.find('h2').text.strip()

# 提取新闻链接
link
= news_item.find('a')['href']

# 提取发布时间
time_string
= news_item.find('span', class_='publish-time').text.strip()
publish_time
= datetime.strptime(time_string, '%Y-%m-%d %H:%M:%S')

# 将提取的信息存入字典
news_info
= {'title': title, 'link': link, 'publish_time': publish_time}
news_list.append(news_info)

except Exception
ase:
# 异常处理,打印异常信息
print(f
"处理新闻时发生异常:{e}")

# 打印提取的新闻信息
for news_info innews_list:
print(f
"标题:{news_info['title']}")
print(f
"链接:{news_info['link']}")
print(f
"发布时间:{news_info['publish_time']}")
print(
"\n")

在这个案例中,我们首先发送HTTP请求获取新闻页面的HTML内容,然后使用BeautifulSoup解析HTML。通过观察网页结构,我们找到包含新闻信息的
<div>
标签,然后逐一提取新闻标题、链接和发布时间。最后,将提取的信息存入字典,并打印出来。

这个案例演示了如何结合BeautifulSoup4和Requests库进行实际的网页信息提取,是学习和使用爬虫技术的一个有趣的实践。

反爬虫策略应对

在进行爬虫开发时,经常会遇到一些网站采取了反爬虫策略,为了防止被封禁或识别为爬虫,我们可以采取以下一些策略:

设置合理的请求头: 模拟正常浏览器的请求头,包括User-Agent等信息,以减小被识别为爬虫的概率。

使用IP代理: 轮换使用不同的IP地址,避免在短时间内发送大量请求,减轻服务器负担。

随机化请求频率: 随机化请求的时间间隔,避免定时、有规律的请求,减少被识别为爬虫的可能性。

处理验证码: 一些网站设置了验证码来防止爬虫,当遇到验证码时,需要通过程序自动识别或手动处理。

模拟浏览器行为: 使用工具如Selenium模拟浏览器行为,使爬虫请求更加接近正常用户的行为。

监测网站更新: 定期检查目标网站是否有更新,以及是否有新的反爬虫策略。灵活调整爬虫策略以适应变化。

这些策略并不是一劳永逸的,不同的网站可能需要采用不同的应对方法。在实际爬虫开发中,根据目标网站的具体情况,灵活选择合适的策略是非常重要的。

希望这篇技术博客对你在爬虫开发中有所帮助,祝你在爬虫的探索中取得丰硕的成果!

总结

在这篇技术博客中,我们深入探讨了爬虫解析器BeautifulSoup4的基础知识和实战应用。通过介绍BeautifulSoup4的简介、安装以及基本用法,我们了解了如何使用它解析HTML和XML文档,以及如何通过标签选择器和属性选择器提取所需信息。

通过一个实际案例,我们演示了如何结合BeautifulSoup4和Requests库爬取新闻网站的信息,提取标题、链接和发布时间等关键信息。这个案例帮助读者更好地理解BeautifulSoup4在实际爬虫项目中的应用。

随后,我们探讨了爬虫的道德准则,提到了尊重网站
robots.txt
文件、设置适当的爬虫速率、标识爬虫身份等原则。同时,我们强调了安全性的重要性,包括处理异常情况、遵循法律法规、防范反爬虫机制等。

接着,我们分享了一系列实用技巧与优化建议,包括使用Session保持会话、避免频繁请求相同页面、使用多线程或异步请求等。这些技巧有助于提高爬虫效率、降低被封禁风险,并使爬虫项目更加稳定可靠。

在最后的章节中,我们通过案例实战展示了爬虫开发中的一些挑战,如反爬虫策略的应对。我们介绍了一些应对措施,包括设置合理的请求头、使用IP代理、随机化请求频率等。这些策略帮助读者更好地理解如何在面对反爬虫机制时保持爬虫的有效性。

点击关注,第一时间了解华为云新鲜技术~

为什么是ML策略?

从一个启发性的例子开始讲,假设正在调试的猫分类器,经过一段时间的调整,系统达到了90%准确率,但对的应用程序来说还不够好。

可能有很多想法去改善的系统,比如,可能想去收集更多的训练数据吧。或者会说,可能的训练集的多样性还不够,应该收集更多不同姿势的猫咪图片,或者更多样化的反例集。或者想再用梯度下降训练算法,训练久一点。或者想尝试用一个完全不同的优化算法,比如
Adam
优化算法。或者尝试使用规模更大或者更小的神经网络。或者想试试
dropout
或者
\(L2\)
正则化。或者想修改网络的架构,比如修改激活函数,改变隐藏单元的数目之类的方法。

当尝试优化一个深度学习系统时,通常可以有很多想法可以去试,问题在于,如果做出了错误的选择,完全有可能白费6个月的时间,往错误的方向前进,在6个月之后才意识到这方法根本不管用。比如,见过一些团队花了6个月时间收集更多数据,却在6个月之后发现,这些数据几乎没有改善他们系统的性能。所以,假设的项目没有6个月的时间可以浪费,如果有快速有效的方法能够判断哪些想法是靠谱的,或者甚至提出新的想法,判断哪些是值得一试的想法,哪些是可以放心舍弃的。

很多大学深度学习课程很少提到这些策略。事实上,机器学习策略在深度学习的时代也在变化,因为现在对于深度学习算法来说能够做到的事情,比上一代机器学习算法大不一样。希望这个篇幅的策略能帮助提高效率,让你们的深度学习系统更快投入实用。

C#/.NET/.NET Core 学习、工作、面试指南

让现在的自己不再迷茫 ✨✨✨。

DotNetGuide 简介


现如今网上关于 Java、前端、Android、Golang...等相关技术的学习资料、工作心得、面试指南一搜都是一大把,但是咱们 C#/.NET 的相关学习资料、工作心得、面试指南都是寥寥无几。
我在微信群和知乎里面经常会看到这样的提问:有没有好的 C#/.NET 相关的学习书籍、视频教程、项目框架和常见面试题推荐的?
。并不是没有人写,而是因为网上的资料和文章太零散了,缺少一个这方面汇总的知识库。作为一名.NET 开发者而言当然是不想让这种现象普遍存在的,建立 DotNetGuide 知识库的初衷就是为了记录、收集和总结优质的.NET 相关学习资料(学习路线、视频、文章、书籍、项目、框架...等)、开发实战教程、开发必备工具、常见面试题、面试须知、简历模板、以及自己在学习和工作中的一些微薄见解,希望可以帮助到那些想要入门学习.NET 或者正在使用.NET 的同学。一个人的力量是十分有限的,由衷的欢迎大家可以在 Issues 中投稿或者直接提交 PR 一起完善我们的 C#/.NET/.NET Core 学习、工作、面试指南知识库

上篇讲到  c# Unit of Work 知识分享时,对于创建DBContext 的封装没有讲到,这次分享跟大家


public interfaceIDbContextFactory
{
DbContext CreateDbContext(
stringconnectionString);
}
/// <summary> ///这里实现的思路是根据用户输入的代码来决定链接的是哪个数据库,从而实现数据库多租户的方式///DbContextFactory 将能够在一个请求周期内创建并复用单个 DbContext 实例/// </summary> public classDbContextFactory : IDbContextFactory
{
private readonlyIServiceScopeFactory _scopeFactory;privateDbContext _dbContext;publicDbContextFactory(IServiceScopeFactory scopeFactory)
{
_scopeFactory
=scopeFactory;
}
public DbContext CreateDbContext(stringconnectionString)
{
if (_dbContext != null)
{
return_dbContext;
}
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException("链接字符串是空的...");
}
var scope =_scopeFactory.CreateScope();var optionsBuilder = new DbContextOptionsBuilder<NoteDbContext>();
optionsBuilder.UseMySql(connectionString,
new MySqlServerVersion(newVersion()));
optionsBuilder.AddInterceptors(
newLoggingInterceptor());
_dbContext
= newNoteDbContext(optionsBuilder.Options);return_dbContext;
}
}

IDbContextFactory接口和实现

到这里我查了一些资料,得出了在.netcore中直接使用AddDbContext 与使用我封装的DbContextFactory 区别说明,如果有错误,请各位指正


/*使用 AddDbContext
当使用 AddDbContext 方法注册 DbContext 时:
生命周期管理:ASP.NET Core DI 容器自动处理 DbContext 的生命周期。通常,DbContext 是作为 Scoped 服务注册的,这意味着每个 HTTP 请求都会创建一个新的 DbContext 实例,并且在请求结束时自动释放。
配置简化:AddDbContext 提供了一个地方来配置数据库连接和其他选项,使得配置更集中和一致。
集成:这种方式与 ASP.NET Core 的其他功能(如中间件、过滤器、控制器等)紧密集成,允许在这些组件中通过构造函数注入轻松获取 DbContext 实例。
连接池:对于某些数据库提供程序(如 SQL Server),AddDbContext 允许使用连接池(通过 AddDbContextPool 方法),这可以提高性能,因为它重用连接实例而不是每次都创建新的。
例子:

builder.Services.AddDbContext<NoteDbContext>(options =>
options.UseMySql(
builder.Configuration.GetConnectionString("YourConnectionStringName"),
new MySqlServerVersion(new Version())
)
);

使用 DbContextFactory
当手动创建 DbContextFactory 并使用它来创建 DbContext 实例时:
控制:对 DbContext 的创建有更多的控制,可以在需要的时候创建和释放 DbContext,而不是依赖于请求的生命周期。
灵活性:这种方法在某些特殊场景下很有用,比如在 Singleton 服务中需要使用 DbContext,或者在非 HTTP 请求的环境(如后台任务)中需要创建 DbContext。
手动管理:需要手动管理 DbContext 的生命周期,包括创建、使用和释放。
复杂性:相比于 AddDbContext,手动创建 DbContext 可能会增加代码的复杂性和出错的可能性。
例子:

public class DbContextFactory : IDbContextFactory
{
// ... 实现 DbContextFactory 的代码 ...
}
builder.Services.AddScoped<IDbContextFactory, DbContextFactory>();

总结
如果应用程序遵循标准的 ASP.NET Core 请求处理模式,并且希望利用框架提供的便利性和集成,那么使用 AddDbContext 是更好的选择。
如果需要在不同的环境中创建 DbContext,或者需要更细粒度的控制 DbContext 的创建和销毁,那么使用 DbContextFactory 可能更合适。
在大多数情况下,推荐使用 AddDbContext 方法,因为它简化了配置和管理,同时提供了与 ASP.NET Core 框架的紧密集成。只有在特定场景下,当标准方法不满足需求时,才考虑使用 DbContextFactory。
*/

AddDbContext 与DbContextFactory 区别说明


 public classNoteDbContext : DbContext
{
public NoteDbContext(DbContextOptions<NoteDbContext>options)
:
base(options)
{

}
public NoteDbContext(DbContextOptions<DbContext>options)
:
base(options)
{

}
protected override voidOnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
/*1.安装 Microsoft.EntityFrameworkCore.Proxies 包。
2.在 OnConfiguring 方法或在设置 DbContextOptionsBuilder 时调用 UseLazyLoadingProxies()。
3.导航属性必须是 virtual,这样代理才能重写它们。
4.实体类不能是 sealed,因为代理需要派生自它们。
*/ /*延迟加载可以使初始查询更快,因为它不会加载所有相关的数据。
然而,如果不当使用,它可能会导致 N+1 查询问题,这是指每次访问导航属性时都会执行一个新的数据库查询,
这可能会在不经意间导致大量的数据库请求,从而影响应用程序的性能。

因此,当使用延迟加载时,应该注意:
确保了解何时会触发延迟加载。
在性能敏感的代码路径中,可能需要显式地预加载(eager load)相关数据,以避免 N+1 查询问题。
监控和优化数据库查询,以确保应用程序的性能。
*/optionsBuilder.UseLazyLoadingProxies();//base.OnConfiguring(optionsBuilder); }public DbSet<PrincipalInfo> PrincipalInfoDbSet { get; set; }
}

DbContext的具体配置

缓存也是一把梭项目的标配,从业多年,有事无事set/getCache来一梭子。

夜深人静的时候,头脑里冷不丁会出现一些问题,我竟一时无法自圆其说。

  1. 已经有cpu多级缓存、操作系统page cache,那为什么还需要定义应用缓存?
  2. 应用的多个副本缓存了同一份数据库数据, 怎么保证这些多副本的缓存一致性?


  1. 缓存在计算机体系中的地位

  2. 缓存和缓冲的区别

  3. 使用缓存时的业务考量?

  • 数据一致性要求不那么严格的场景
  • 设计模式: 惰性、直写
  1. 使用缓存时的技术考量?
  • 缓存与数据库一致性
  • 过期策略
  • 驱逐策略


https://course.ccs.neu.edu/cs5600/paging-caching.html

缓存在计算机体系中的定位

内存层次结构

  1. 寄存器
  2. CPU Cache(L1,L2,L3)
  3. RAM (内存/主存)
  4. Disk or SSD(外存/辅助存储)

静态体系架构

  • 低级的缓存包含更多的存储空间,高级的缓存有更好的存取速度(低延迟)。
  • RAM Page cache作为磁盘缓存,CPU cache是RAM的缓存, 寄存器数据来自CPU Cache。

动态逻辑流程

  • 在搜索内存数据, 自上向下搜素(CPU、内存、磁盘),当在某层找到数据时,将在该层的较高层级保存一份副本, 以便将来可以迅速找到该内容。

如果在CPU Cache中没找到数据,我们叫Cache miss;
如果在RAM内存中没找到数据,我们叫Page miss/Page fault

  • 根据数据传输的时间/空间局部性原理,一次传输整个块(而不是单个字节或内存字)时的数据传输效率更高 。
    根据这个设计:
    • 硬盘一次只能传输一个磁盘块,而不是单个字节, 页通常是磁盘块的软件视图,磁盘块、页通常是4KB
    • CPU缓存的数据块通常只有32B

Q:已经有cpu cache, page cache, 为什么我们还需要自定应用义程序cache。

A:不管是cpu级别的cpu cache,还是操作系统维护的page cache,都是对于最近访问数据的缓存, 不针对特定的应用程序,机制对于程序员是透明的。

程序员日常工作的背景是快速灵活的利用内存数据,而不是cpu cache/page cache仅缓存最近访问的数据块(临近32B/4KB),应用程序需要缓存的数据底层存储可能是分散的或访问频次不固定,故应用程序需要做自定义的业务缓存。


作为应用程序员,我们普遍说的Cache指的是应用程序Cache。

2. 缓存 vs 缓冲

缓存是对数据一致性要求不那么严格的一种存取技术,利用内存访问比外设访问速度快的特性, 最终目的是加快入站请求的处理速度。

缓冲是提供一块内存区域,用于入站请求频繁地写操作,之后一次性写入到底层的外设,最终目的是减轻对外设的频繁访问。

3. 使用缓存的业务考量

亚马逊的缓存最佳实践

  • 使用缓存值是否安全。同一段数据在不同的上下文中可能具有不同的一致性要求。例如,在线结账期间,您需要物品的确切价格,因此不适合使用缓存,但在其他页面上,价格晚几分钟更新不会给用户带来负面影响。
  • 对该数据而言,维持缓存是否高效? 某些应用程序会生成不适合缓存的访问模式。 例如,查询频繁变化的大型数据集的键空间,在这种情况下,保持缓存更新可能会抵消缓存带来的优势。
  • 数据结构是否适合缓存? 结构有无schema? or 聚合信息/独立信息?

缓存的产生的方式:

  • 惰性缓存: 仅在应用程序实际请求对象时才填充缓存
  • 直写: 缓存在数据库更新时实时更新,由特定应用程序或者后台程序更新,避免了缓存未命中,可帮助应用程序更好、更快捷地运行

对于第二个问题, 如何维护多个副本的缓存一致?
知乎经典回答:遇事先问“要不要”,而不是直接问”怎么做“,如果定位为缓存,那么本来就有可能是过期的数据; 使用直写来尽快保持一致, 要求更严格就不是缓存了,那就是分布式一致性(CAP理论,共识算法)。

4. 使用缓存的技术考量

4.1 缓存和数据库一致性

  1. cache-aside
    :(旁路缓存) 强调应用程序App与数据库交互, Cache组件作为旁路。
  • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
  • 更新: 先更新数据库中的数据,再删除缓存中的数据。
  1. read-through/write-through: (读穿/写穿) 强调App与Cache组件交互
  • 先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。
  • 更新: 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。

旁路缓存与读/写穿缓存的差异在于 :谁来填充Database:App还是cache组件。 1,2中的写缓存和写数据库的行为是贯序同步的。

  1. write back:(写回)在更新数据时,只更新Cache,标记Cache是脏数据,然后立马返回;对于数据库的更新会通过批量异步更新的方式。

写回策略一般用在计算机系统中(上面的CPU Cache和文件系统的Pache Cahce)。


首先要知道缓存过期和缓存驱逐是不同的关注点, 我们以redis为例。

redis 作为内存键值对数据库, 所有的KV都是以全局字典来实现,带有过期时间的kv键值对也是维护在一个独立的字典中。

4.2 redis缓存过期

redis带有过期时间的KV项,并不是到期就被立即清除的, 考虑:

  • 到期删除: 每一个设置了过期时间的kv项附带一个计时器, 不消费内存,但是消耗cpu资源。
  • 惰性删除: 每次访问时,先去名单中判断该k是否需要清理,不占用cpu, 但是有可能kv项始终没人访问,造成过期的kv项内存堆积。
  • 定期删除: 每隔一段时间从带过期时间的kv字典中清理一部分。

redis以
惰性删除+定期删除(默认100ms)
来实现清理缓存过期KV项, 以实现cpu和mem的平衡使用。

(当然这2种策略还是会有一部分过期的kv值未能删除)

4.3 redis缓存驱逐(内存淘汰)

redis是内存键值对数据库,存储受限于内存。考虑

  • 驱逐目的: 避免无限制占用内存
  • 驱逐时机:
    maxmemory
    : 接近或者达到这个值会触发缓存驱逐。
  • 驱逐策略:
    maxmemory-policy
    ,8种策略(4维度和2种范围的叉积的集合),默认是
    noeviction

https://cloud.tencent.com/developer/article/2045330

# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.


分享一个与缓存有关的OOM案例:

应用定时(1min)滚动设置缓存KV项(1h),一开始使用golang
bluele/gcache

2.5k star 内存缓存库, 支持多种驱逐策略(LFU, LRU and ARC),表现是一个固定长度的map。

func main() {
  gc := gcache.New(1000).   // 缓存项容量, 驱逐时机
    LRU().                  // 驱逐策略
    Build()
  gc.Set("key", "value")
}

注意看gcache的驱逐时机是
基于缓存项容量,与内存无关

一开始缓存项容量设置的比较大,导致不容易触发gcache的KV项驱逐,实际上这个时候gcache占据的内存在滚动增长,最终应用OOM。

  • 案例可以通过设置较小的缓存项容量来解决: 促使尽快达到驱逐阈值
  • 案例也可以切换到
    patrickmn/go-cache
    缓存库来解决: 搭配合适的过期时间和定时清理过期项的周期。

7.7k star 表现就类似redis, 具备定期删除过期项,释放空间的能力(purges expired items every 10 minutes),

// Create a cache with a default expiration time of 5 minutes, and which
	// purges expired items every 10 minutes
	c := cache.New(5*time.Minute, 10*time.Minute)

	// Set the value of the key "foo" to "bar", with the default expiration time
	c.Set("foo", "bar", cache.DefaultExpiration)

	// Set the value of the key "baz" to 42, with no expiration time
	// (the item won't be removed until it is re-set, or removed using
	// c.Delete("baz")
	c.Set("baz", 42, cache.NoExpiration)


所以,除了SetCache(k,v)一把梭外,开发者应有更多前置心路历程:

缓存在计算机系统中的地位、缓存的适用性、缓存的使用方式、 缓存作为内存存储的过期、驱逐实践......

本文抛砖引玉,望构建更完整的缓存知识体系, 自勉。