
🔎大家好,我是ZTLJQ,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
📝个人主页-ZTLJQ的主页
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝📣系列果你对这个系列感兴趣的话
专栏 - Python从零到企业级应用:短时间成为市场抢手的程序员
✔说明⇢本人讲解主要包括Python爬虫、JS逆向、Python的企业级应用
如果你对这个系列感兴趣的话,可以关注订阅哟👋
超越关系的边界
在数据驱动的时代,关系型数据库(RDBMS)凭借其严格的结构和强大的事务能力,一直是数据存储的中流砥柱。然而,随着互联网应用的爆炸式增长,出现了许多传统RDBMS难以应对的新挑战:非结构化或半结构化数据 、海量数据的水平扩展 、高并发读写 、实时数据分析等。
正是在这样的背景下,NoSQL(Not Only SQL)数据库应运而生。它并非要取代SQL,而是提供了一种更灵活、更具扩展性的数据存储方案,以应对多样化的业务场景。本篇博客将带您深入了解NoSQL的核心理念,并通过Python代码示例,探索MongoDB和Redis这两大主流NoSQL数据库的奥秘。
第一部分:NoSQL概览------为何选择"非关系"?
1.1 NoSQL的核心特征
与关系型数据库的"表"和"行"不同,NoSQL数据库采用了多种不同的数据模型,以适应不同的使用场景。
- 灵活的数据模型: 不强制要求预定义固定的表结构(Schema)。数据可以是键值对、文档、列族或图结构,这使得存储非结构化数据(如JSON、XML)变得非常自然。
- 横向扩展 (Horizontal Scalability): NoSQL数据库天生为分布式设计,可以通过增加服务器节点来轻松扩展存储容量和处理能力,以应对PB级别的数据和百万级的并发请求。
- 高性能: 为了获得更高的性能,NoSQL数据库通常会牺牲一部分ACID特性(尤其是强一致性),采用BASE理论(基本可用、软状态、最终一致性),从而在特定场景下(如读多写少、缓存)提供远超RDBMS的性能。
- 高可用性: 通过数据复制(Replication)等机制,NoSQL数据库能保证在部分节点故障时,系统依然可以正常服务。
1.2 主要NoSQL数据库类型
- 文档数据库 (Document Store) : 将数据以文档(通常是JSON或BSON格式)的形式存储。MongoDB是其中的佼佼者,非常适合内容管理系统、用户资料、产品目录等场景。
- 键值存储 (Key-Value Store) : 以键(Key)和值(Value)的简单形式存储数据,查找速度极快。Redis是最著名的代表,广泛用于缓存、会话存储、排行榜、消息队列等。
- 列族存储 (Column-Family Store) : 将数据按列而非按行存储,非常适合大数据分析和时间序列数据。Apache Cassandra 和HBase是典型代表。
- 图数据库 (Graph Database) : 专门用于存储实体(节点)及其关系(边),非常适合社交网络、推荐系统、欺诈检测等需要处理复杂关联关系的场景。Neo4j是其中的领导者。
第二部分:文档数据库的王者------MongoDB
MongoDB (pip install pymongo) 是最流行的文档数据库。它将数据存储为BSON(Binary JSON)格式的文档,这些文档被组织在"集合"(Collection)中,类似于关系型数据库中的"表"。
2.1 实战案例:使用pymongo构建一个博客平台
假设我们要构建一个博客平台,需要存储文章、评论和用户信息。在关系型数据库中,我们会创建posts, comments, users等多个表并通过外键关联。而在MongoDB中,我们可以更灵活地设计数据结构。
第一步:环境准备与连接
bash
pip install pymongo
python
from pymongo import MongoClient
from datetime import datetime
# 连接到MongoDB服务器 (本地默认地址)
# 如果是远程服务器,需要替换为实际的地址,如: "mongodb://username:password@host:port/"
client = MongoClient('localhost', 27017)
# 选择一个数据库 (如果不存在,会在首次写入时创建)
db = client['blog_platform']
# 选择一个集合 (如果不存在,会在首次写入时创建)
posts_collection = db['posts']
代码解析:
MongoClient: 连接到MongoDB服务器的入口点。db['collection_name']: 获取一个数据库实例和集合引用。MongoDB的数据库和集合理论上都是"懒创建"的,只有在真正进行写入操作时才会物理创建。
第二步:插入数据
python
# 准备一篇博客文章的数据 (一个Python字典,MongoDB会将其转换为BSON文档)
post_data = {
"title": "Exploring the Wonders of NoSQL",
"author": "Alice Johnson",
"tags": ["NoSQL", "MongoDB", "Database"], # 数组字段
"content": "In this post, we'll dive deep into the world of NoSQL databases...",
"date_created": datetime.utcnow(), # 日期字段
"published": True,
"metadata": { # 嵌套文档
"views": 0,
"likes": 0
},
"comments": [ # 嵌套数组,包含文档
{
"author": "Bob Smith",
"text": "Great article!",
"date_posted": datetime.utcnow()
}
]
}
# 插入单个文档
result = posts_collection.insert_one(post_data)
print(f"Inserted document with ID: {result.inserted_id}")
# 批量插入多篇文章
more_posts = [
{
"title": "The Rise of Python",
"author": "Charlie Brown",
"tags": ["Python", "Programming"],
"content": "Python has become one of the most popular languages...",
"date_created": datetime.utcnow(),
"published": True
},
{
"title": "Understanding Asynchronous Programming",
"author": "Alice Johnson",
"tags": ["Python", "Async"],
"content": "Async programming can be tricky but powerful...",
"date_created": datetime.utcnow(),
"published": False # 草稿
}
]
result_multi = posts_collection.insert_many(more_posts)
print(f"Inserted {len(result_multi.inserted_ids)} documents.")
代码解析:
insert_one(): 插入单个文档。insert_many(): 批量插入多个文档,性能更高。- 灵活性体现 : 数据结构可以随时改变。
comments是一个包含文档的数组,metadata是一个嵌套文档,这些在关系型数据库中需要复杂的表结构和JOIN操作,而在MongoDB中只需一个文档即可。
第三步:查询数据
python
# 查询所有已发布的文章
published_posts = posts_collection.find({"published": True})
print("\nPublished Posts:")
for post in published_posts:
print(f"- {post['title']} by {post['author']}")
# 查询特定作者的文章
alice_posts = posts_collection.find({"author": "Alice Johnson"})
print("\nPosts by Alice Johnson:")
for post in alice_posts:
print(f"- {post['title']} (Published: {post['published']})")
# 查询包含特定标签的文章
python_posts = posts_collection.find({"tags": "Python"}) # 查找tags数组中包含"Python"的文档
print("\nPosts tagged with 'Python':")
for post in python_posts:
print(f"- {post['title']}")
# 查询单个文档
single_post = posts_collection.find_one({"title": "The Rise of Python"})
if single_post:
print(f"\nFound single post: {single_post['title']}")
# 复杂查询: 发布日期大于某天,且作者为Alice
from datetime import timedelta
cutoff_date = datetime.utcnow() - timedelta(days=1)
complex_query = posts_collection.find({
"date_created": {"$gt": cutoff_date},
"author": "Alice Johnson"
})
print("\nRecent posts by Alice:")
for post in complex_query:
print(f"- {post['title']}")
代码解析:
find(query): 返回一个游标对象,可以迭代获取所有匹配的文档。find_one(query): 返回第一个匹配的文档,如果没有则返回None。$gt: MongoDB的查询操作符,表示"greater than"。类似的还有$lt,$gte,$in,$regex等,提供了强大的查询能力。- 查询灵活性 : 可以轻松查询嵌套字段(如
metadata.views)和数组元素。
第四步:更新和删除数据
python
# 更新单个文档: 为某篇文章增加浏览量
posts_collection.update_one(
{"title": "Exploring the Wonders of NoSQL"}, # 查询条件
{"$inc": {"metadata.views": 1}} # 更新操作符 $inc 用于对数值字段进行增量操作
)
print("\nUpdated view count for 'Exploring the Wonders of NoSQL'.")
# 更新多个文档: 将所有Alice的草稿发布
posts_collection.update_many(
{"author": "Alice Johnson", "published": False}, # 查询条件
{"$set": {"published": True}} # 更新操作符 $set 用于设置字段值
)
print("Published all drafts by Alice Johnson.")
# 删除单个文档
posts_collection.delete_one({"title": "Understanding Asynchronous Programming"})
print("Deleted the draft post 'Understanding Asynchronous Programming'.")
# 删除多个文档
posts_collection.delete_many({"author": "Charlie Brown"})
print("Deleted all posts by Charlie Brown.")
# 再次查询所有文章,查看更改
all_posts_after_changes = posts_collection.find()
print("\nAll posts after updates and deletions:")
for post in all_posts_after_changes:
print(f"- {post['title']} (Published: {post['published']})")
代码解析:
update_one()/update_many(): 分别用于更新单个或多个匹配的文档。delete_one()/delete_many(): 分别用于删除单个或多个匹配的文档。$inc,$set: MongoDB强大的更新操作符,可以直接在数据库层面进行原子操作。
第三部分:高性能键值存储------Redis
Redis (pip install redis) 是一个开源的、基于内存的键值存储系统。由于其数据存储在内存中,所以读写速度极快,常被用作数据库、缓存和消息中间件。
3.1 实战案例:使用redis-py构建一个用户会话系统和缓存层
让我们看看如何用Redis来解决Web开发中的两个经典问题。
第一步:环境准备与连接
bash
pip install redis
python
import redis
import json
import time
# 连接到Redis服务器 (本地默认地址)
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# decode_responses=True 会自动将结果解码为Python字符串,而不是字节
第二部分A:作为缓存层
python
def expensive_database_query(user_id):
"""
模拟一个耗时的数据库查询
"""
print(f"Simulating expensive query for user {user_id}...")
time.sleep(2) # 模拟2秒的延迟
return {"id": user_id, "name": f"User_{user_id}", "data": "some complex data..."}
def get_user_cached(user_id):
"""
从Redis缓存获取用户数据,如果缓存未命中则查询数据库并存入缓存
"""
cache_key = f"user_profile:{user_id}"
# 尝试从Redis获取数据
cached_data = r.get(cache_key)
if cached_data:
print(f"Cache HIT for user {user_id}")
# Redis中存储的是JSON字符串,需要反序列化
return json.loads(cached_data)
print(f"Cache MISS for user {user_id}")
# 缓存未命中,查询数据库
user_data = expensive_database_query(user_id)
# 将查询结果存入Redis,设置过期时间为300秒 (5分钟)
r.setex(cache_key, 300, json.dumps(user_data))
print(f"Data cached for user {user_id}")
return user_data
# --- 测试缓存效果 ---
print("--- First call (will query DB and cache) ---")
start = time.time()
user1 = get_user_cached(123)
print(f"Time taken: {time.time() - start:.2f}s\n")
print("--- Second call (should hit cache) ---")
start = time.time()
user1_cached = get_user_cached(123)
print(f"Time taken: {time.time() - start:.2f}s\n")
代码解析:
r.get(key): 获取key对应的value。r.setex(key, seconds, value): 设置key-value,并指定过期时间(以秒为单位)。这是缓存场景的核心操作。- 缓存逻辑: 当请求到来时,先查Redis,有则直接返回;没有则查数据库,再将结果存入Redis。这极大地减轻了后端数据库的压力。
第二部分B:作为用户会话存储
python
import secrets
def create_user_session(user_id):
"""
为用户创建一个新的会话
"""
session_id = secrets.token_urlsafe(32) # 生成一个安全的随机会话ID
session_data = {
"user_id": user_id,
"login_time": time.time(),
"ip_address": "192.168.1.100" # 这里可以是真实的IP
}
# 将会话数据存储到Redis,key为 session:<session_id>
session_key = f"session:{session_id}"
r.setex(session_key, 3600, json.dumps(session_data)) # 会话有效期1小时
return session_id
def get_user_from_session(session_id):
"""
根据会话ID获取用户信息
"""
session_key = f"session:{session_id}"
session_json = r.get(session_key)
if session_json:
session_data = json.loads(session_json)
# 可以在这里刷新会话过期时间
r.expire(session_key, 3600)
return session_data
return None
# --- 测试会话管理 ---
print("--- Creating a session for user 456 ---")
session_token = create_user_session(456)
print(f"Created session token: {session_token}\n")
print("--- Retrieving user from session ---")
session_info = get_user_from_session(session_token)
if session_info:
print(f"Session data: {session_info}")
else:
print("Session not found or expired.")
代码解析:
secrets.token_urlsafe(32): 生成一个安全的、URL安全的随机令牌,非常适合用作会话ID。r.expire(key, seconds): 可以动态修改一个key的过期时间,用于实现"滑动过期"(用户活跃时延长会话)。- 会话存储: 传统上会话信息存储在服务器内存或数据库中,使用Redis作为会话存储可以实现高可用和水平扩展,是现代Web应用的常用方案。
第四部分:NoSQL vs. RDBMS 选择指南
| 特性 | NoSQL | RDBMS |
|---|---|---|
| 数据模型 | 灵活,支持文档、键值、图等 | 固定的表结构 |
| 扩展性 | 水平扩展(加机器) | 垂直扩展(加配置)为主 |
| 事务支持 | 通常支持单文档/单键事务,复杂跨文档事务支持有限 | 支持复杂的ACID跨表事务 |
| 查询语言 | 各自的API和查询语法(如MongoDB的Query Language) | 标准SQL |
| 适用场景 | 大数据、高并发、灵活结构、实时分析 | 结构化数据、强一致性、复杂关联查询 |
结论:
- 选择NoSQL: 当你需要处理海量数据、需要极高的并发读写、数据结构可能频繁变化、或者对数据一致性要求不是最高(可以接受最终一致性)时。
- 选择RDBMS: 当你的数据结构稳定、业务逻辑复杂、需要强大的事务保证和复杂的关联查询时。
结语
NoSQL数据库为现代应用架构提供了不可或缺的弹性与性能。MongoDB以其强大的文档模型满足了内容和用户数据的存储需求,而Redis则以其闪电般的速度成为了缓存和会话管理的首选。
通过本篇博客的学习,您应该已经掌握了:
- NoSQL数据库的核心理念和主要类型。
- 如何使用
pymongo操作MongoDB进行数据的增删改查。 - 如何使用
redis-py构建缓存和会话系统。 - 如何根据业务需求选择合适的数据库技术。