Mongodb与Clickhouse对比

本文详细介绍了如何使用Docker分别安装Clickhouse和MongoDB,并提供了大数据插入脚本进行性能测试。结果显示,Clickhouse在批量插入数据和大数据查询上表现出更高的效率,同时数据压缩更优,占用空间小。对于精准和模糊查询,Clickhouse在数据量较大时耗时更短。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、硬件设施

 watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6Z2S6ZyE,size_12,color_FFFFFF,t_70,g_se,x_16

二、安装数据库

2.1、docker安装clickhouse

1、拉取镜像
docker pull yandex/clickhouse-server

2、启动服务(临时启动,获取配置文件)
docker run --rm -d --name=clickhouse-server \
--ulimit nofile=262144:262144 \
-p 8123:8123 -p 9009:9009 -p 9000:9000 \
yandex/clickhouse-server:latest

3、复制容器中的配置文件到宿主机
提前创建好文件夹,mkdir -p /home/clickhouse/conf/
docker cp clickhouse-server:/etc/clickhouse-server/config.xml /home/clickhouse/conf/config.xml
docker cp clickhouse-server:/etc/clickhouse-server/users.xml /home/clickhouse/conf/users.xml

4、停止第2步启动的ck容器(clickhouse-server)

5、修改密码 第3步的配置文件
vi /home/clickhouse/conf/users.xml,在password标签中加上密码,可以使用加密密码

6、重启启动
docker run -d --name=clickhouse-server \
-p 8123:8123 -p 9009:9009 -p 9000:9000 \
--ulimit nofile=262144:262144 \
-v /home/clickhouse/data:/var/lib/clickhouse:rw \
-v /home/clickhouse/conf/config.xml:/etc/clickhouse-server/config.xml \
-v /home/clickhouse/conf/users.xml:/etc/clickhouse-server/users.xml \
-v /home/clickhouse/log:/var/log/clickhouse-server:rw \
yandex/clickhouse-server:latest

7、进入ck容器
docker exec -it 82 clickhouse-client  --user=default --password=your_password
select version() # 查看版本

2.2、docker安装mongodb

配置文件:/home/mongo/configdb/mongod.conf
storage:
    dbPath: /data/db
    journal:
        enabled: true
    engine: wiredTiger
security:
    authorization: enabled
net:
    port: 27017
    bindIp: 192.168.xx.xx
容器启动:
docker run -d \
-p 27017:27017 \
--name mongo \
-v /home/mongo/db:/data/db \
-v /home/mongo/configdb:/data/configdb \
-v /etc/localtime:/etc/localtime \
mongo -f /data/configdb/mongod.conf

三、大数据插入脚本

import json, time
import pymongo,traceback
from clickhouse_driver import Client
import uuid
import random

# 装饰器统计运行耗时
def coast_time(func):
    def fun(*args, **kwargs):
        t = time.perf_counter()
        result = func(*args, **kwargs)
        print(f'func {func.__name__} coast time:{time.perf_counter() - t:.8f} s')
        return result
    return fun

class MyEncoder(json.JSONEncoder):
    """
    func:
        解决Object of type 'bytes' is not JSON serializable
    """
    def default(self, obj):
        if isinstance(obj, bytes):
            return str(obj, encoding='utf-8')
        return json.JSONEncoder.default(self, obj)

create_task_table = """CREATE TABLE IF NOT EXISTS task(\
    `_id` String,\
    `task_name` String,\
    `task_size` UInt16,\
    `status` UInt8
    )
    ENGINE = MergeTree() PRIMARY KEY _id;
"""
ck_client = Client(host='192.168.12.199', port=9000, database="testdb", user='default', send_receive_timeout=20)
mongo_client = pymongo.MongoClient("mongodb://192.168.12.199:27017/")
mongo_db = mongo_client["testdb"]
mongo_col = mongo_db["task"]

@coast_time
def insert_mongo_task_data(total, patch):
    """
    func:批量插入任务数据到mongo
    """
    mongo_col.drop()
    sig = 0
    data_list = []
    for i in range(total):
        sig += 1
        try:
            dicts = {
                '_id':str(uuid.uuid1()),
                'task_name': 'task_' + str(i),
                'task_size': i,
                'status': random.choice([0, 1])
            }
            data_list.append(dicts)
            if sig == patch:
                mongo_col.insert_many(data_list)
                sig = 0
                data_list=[]
        except Exception as e:
            print("task name :%s process failed:%s" % ('task_' + str(i), traceback.print_exc()))
    if len(data_list) >0:
        mongo_col.insert_many(data_list)

@coast_time
def insert_ck_task_data(total, patch):
    """
    func:批量插入任务数据到CK
    """
    ck_client.execute('DROP TABLE IF EXISTS task')
    ck_client.execute(create_task_table)
    sig = 0
    data_list = []
    for i in range(total):
        sig += 1
        try:
            dicts = {
                '_id':str(uuid.uuid1()),
                'task_name': 'task_' + str(i),
                'task_size': i,
                'status': random.choice([0, 1])
            }
            data_list.append(dicts)
            if sig == patch:
                ck_client.execute("INSERT INTO task(*) VALUES", data_list, types_check=True)
                sig = 0
                data_list=[]
        except Exception as e:
            print("task name :%s process failed:%s" % ('task_' + str(i), traceback.print_exc()))
    if len(data_list) >0:
        ck_client.execute("INSERT INTO task(*) VALUES", data_list, types_check=True)


insert_ck_task_data(100000000, 10000)
insert_mongo_task_data(100000000, 10000)
批次10000
插入条数
mongo耗时
ck耗时
Mongo大小
Ck大小
1000
0.01972508s
0.01732014s
99.5K
12K
10000
0.12277857s
0.08004815s
1004.8K
119K
100000
1.12529528s
0.73075602s
9.0M
1.9M
1000000
10.92156150s
7.17739819s
100M
49M
10000000
108.91806854s
72.16343116s
1009.8M
117M
100000000
1189.25558783s
748.89750133s
10G
1.1G
 
不同条数精准查询耗时分析:
插入条数
db.task.find({'task_name':'task_1'}) .explain("executionStats")耗时
select * from testdb.task where
task_name ='task_1' 耗时
1000
0ms
0.004 sec
10000
3ms
0.008 sec
100000
29ms
0.006 sec
1000000
340ms
0.009 sec
10000000
3281ms
0.035 sec
100000000
165762ms
0.626 sec
不同条数模糊查询耗时分析:
插入条数
db.task.find({'task_name':{'$regex':'.*_1.*'}})
.explain("executionStats")耗时
select * from testdb.task where
task_name like '%_1%' 耗时
1000
0ms
0.004 sec
10000
4ms
0.012 sec
100000
42ms
0.022 sec
1000000
468ms
0.077 sec
10000000
5871ms
0.670 sec
100000000
112334ms
21.094 sec
group聚合耗时:
插入条数
db.getCollection("task").aggregate([
       {"$group": {
                "_id":null,
                "total_num":{"$sum":1},
                "total_size":{"$sum":"$task_size"},
                "avg_size":{"$avg":"$task_size"}
     }}
]) 耗时
select count(*), SUM(task_size), AVG(task_size) from task  耗时
100000000
106775ms
0.035 sec
通过上述对比,Clickhouse对数据的压缩更为出色,同样的数据下,占用的空间较小;
批量插入数据,Clickhouse比Mongodb耗时少,而且随着数据量的增大,这种差距也在拉大;
万级别下的查询,Mongodb耗时比Clickhouse少,但超过万级别,随着数据的不断增大,Clickhouse的耗时小于Mongodb。

 

在Java Spring Boot应用程序中实现数据库数据的国际化是一个复杂的过程,因为它不仅涉及到了静态文本资源文件的管理,还涉及到如何将这些本地化的信息映射到动态存储的数据上。以下是几种常见的做法来应对这个问题: ### 方案一:通过枚举或预定义常量集合作为桥梁 如果你的应用场景允许的话,可以预先确定好一部分关键字段的内容形式及其对应的多种语言解释,并把这些内容封装成枚举或者其他固定集合的形式存放在程序内部。每当访问某个具体的记录时,就根据当前环境下的Locale去匹配最合适的描述版本返回给客户端显示。 这种方法适用于那些业务规则相对稳定不变而且预期不会频繁变动的信息类别,如订单状态、支付方式等标准选项列表;然而对于个性化程度较高的非结构化数据则显得力不从心了。 ### 方案二:双表关联设计模式 另一个较为通用的设计思路是在原有实体模型的基础上新增一张或多张“翻译”性质的关系型表格专门用来承载多语言版本的具体表述。以文章为例,原本的一篇文章只对应一行主键ID+标题正文这样的简单关系,现在我们可以构造出一个额外的文章详情表,里面包含了诸如原文id、目标语种标识以及该条目在当地文化环境下应有的呈现形态等内容项。查询的时候依据请求方指定的目标language tag对两张表实施JOIN联接运算获取最终结果集。 这种方案的好处是可以灵活适应未来可能出现的新需求点同时保持良好的扩展性和维护性,但是缺点在于增加了系统的复杂度同时也可能导致一定的性能损耗尤其是在面对海量级规模的数据集时需要谨慎评估其影响范围。 ### 实现步骤详解 - 双表关联法 假定我们要为一篇博客帖子(Post)添加标题和摘要两个部分的支持多国字幕的功能,则可以根据下面流程逐步推进下去: #### 步骤 01. 数据建模 创建原始post实体及其相应的Repository接口用于CRUD基本操作。 ```java @Entity @Table(name = "posts") public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 其他属性略... } interface PostRepository extends JpaRepository<Post,Long>{} ``` 接着再建立第二个名为PostTranslation的POJO类表示每一种特定方言下所特有的那部分内容。 ```java @Entity @ToString(callSuper=true) @NoArgsConstructor(access= AccessLevel.PRIVATE)// Lombok插件生成无参构造器 @Getter @Setter(onMethod_=@NonNull)// 强制要求提供getter/setter方法的同时不允许为空值赋入 @Table(uniqueConstraints={@UniqueConstraint(columnNames={"postId", "locale"})}) public class PostTranslation { @EmbeddedId// 使用组合键代替单一列作为唯一识别码 protected PostTranslationKey pk=new PostTranslationKey(); /** * 组合关键字包括外键引用原生表primary key + 目标语言标记符 */ @Embeddable public static final class PostTranslationKey implements Serializable{ @Column(nullable=false) String locale;// e.g., zh_CN,en_US @ManyToOne(cascade=CascadeType.ALL ) @JoinColumn(name="postId") Post post; // equals 和hashCode 的定义... } private String title; private String summary; } ``` 然后相应地也要有配套的操作仓储层组件帮助我们更高效便捷地之交互。 ```java @Repository public interface IPostTranslationDao extends CrudRepository<PostTranslation,PostTranslation.PostTranslationKey>{ Optional<List<PostTranslation>> findByPk_PostOrderByPk_LocaleAsc(Post target); } ``` #### 步骤 02. Service 层逻辑编写 接下来就是最重要的Service层了,在这里我们需要明确当接受到来自外界关于加载某篇blog article的需求信号后应该采取怎样的行动路线才能满足既定目的——即找到贴切于当下上下文境况的最佳诠释变体并反馈出去。一般而言我们会先判断是否存在精确吻合的translation entry存在,如果没有就退而求其次选用系统默认fallback value也就是英文版吧! ```java @Service @Transactional(readOnly=true) @AllArgsConstructor class BloggingServiceImpl implements IBloggingService { private final IPublicationGateway gateway; private final LocaleResolver resolver; @Override public Mono<BlogArticleDTO> getLocalizedContent(Long postId) { return this.gateway.findById(postId).map(it -> mapToTransferObject(resolver.resolveLocale(), it)); } private BlogArticleDTO mapToTransferObject(Locale preferredLanguageTag, Publication aggregateRoot) { var translations=gateway.findTranslationsByAggregateIdentifierAndOrdering(preferredLanguageTag.getISO3Language()); return translations.orElseGet(()-> new BlogArticleDTO( aggregateRoot.getId(), null, aggregateRoot.getTitle(), aggregateRoot.getDescription() )); } } ``` 请注意这里的`IBloggingService`, `IPublicationGateway`都是简化后的伪代码示意而已实际上还要结合实际情况做出适当调整修改哦~ 另外别忘了注册那个负责解析传入HTTP header里面的Accept-Language参数从而得到正确的RegionInfo的对象实例: ```properties # application.properties 配置示例 spring.mvc.locale=en_US spring.mvc.locales=zh_CN,en_US spring.messages.basename=i18n/messages ``` ```java @Bean public AcceptHeaderLocaleResolver acceptHeaderLocaleResolver(){ AcceptHeaderLocaleResolver result=new AcceptHeaderLocaleResolver(); result.setDefaultLocale(new Locale("en","US")); List<Locale> supportedLanguages = Arrays.asList( new Locale("zh","CN"), new Locale("en","US") ); result.setSupportedLocales(supportedLanguages); return result; } ``` --- 经过上面一系列改造之后我们就成功赋予了一个普通的SpringBoot RESTful API具备处理来自世界各地访客朋友发送过来的要求的能力啦!当然除了以上提到的技术手段之外还有很多其他的途径可以选择,比如直接把所有的localizable string都放进NoSQL store当中去托管或是干脆全部交给第三方云服务商打理等等…总之各有千秋取决于各自的特殊诉求咯。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值