第三章:保存和更新(怎么避免脏数据问题?)

1,553 阅读2分钟

一.更新保证原子性

findOneAndUpdate 是获取mongo文档中第一条满足条件的数据并做修改。该函数是线程安全的,在多个线程中操作,不会对同一条数据进行获取修改,也就是该操作是原子操作。python实现demo如下:

# -*- coding: utf-8 -*-
# @Time    : 2020/8/14 23:49
# @Author  : CC
# @Desc    : mongo_find_modify.py
from utils import mongo_util

collection_test = mongo_util.get_collection("test", "test")
result = collection_test.find_one_and_update({"userid": "2"}, {"$inc": {"rownum": 1}})
print(result)

mongodb4支持事物,能保证多条记录更新的原子性,详细pyhton操作demo如下:

from utils import mongo_util

collection_test = mongo_util.get_collection('test', 'test')

with mongo_util.client.start_session() as s:
    s.start_transaction()
    try:
        doc1 = {"userid": 10}
        collection_test.insert_one(doc1, session=s)
        doc2 = {"userid": 12}
        collection_test.insert_one(doc2, session=s)
        raise Exception('测试抛出异常')
    except:
        s.abort_transaction()
        raise
    s.commit_transaction()

二.数据写入去重

但数据存储中经常会有保证单条记录唯一性的要求,简单说就是去重。实际情况有如下三种处理方案:
1.需要去重字段建立唯一索引

db.test.createIndex({"userid":1})

2.设置主键_id的值为 唯一字段的值,保存的时候如果有重复数据直接覆盖之前的数据

db.test.save({"userid": "5","age":21,"rownum": 1})

3.使用upsert=true去重

db.test.update({"userid": "5"}, {"$set":{"age":21},{"upsert":"true"})
方案1,如果表记录数大会增加额外的存储空间,并且在保存的时候容易忘记异常处理导致程序中断  

方案2,没有方案一的缺点,但是会强制要求主键_id值设置为唯一字段的值;因为是全覆盖,容易导致保存的数据属性丢失  

方案3,没有方案一和方案二的缺点,upsert操作会先在集合中进行数据查找,如果数据已经存在,则更新,否则才插入。数据的查找那就势必会使用索引,mongo索引用的是B树,时间复杂度为Olog(n),而没有索引的情况下则时间复杂度是O(n),数据量越大性能越慢   

以上三种方案可以根据当前数据量来选择。如果数据量比较小选方案1和方案3均可,数据量比较大选择方案二

三.mongodb的锁保证操作原子性

1 锁粒度:

mongo3.0之后的默认引擎WiredTiger,锁的粒度最小是文档级别的  

2 常用操作加入的锁:

insert:库级别的意向读锁(r),表级别的意向读锁(r),文档级别的读锁(R)

update:库级别的意向写锁(w),表级别的意向写锁(w),文档级别的写锁(W)

foreground方式创建索引:库级别的写锁(W)

background方式创建索引:库级别的意向写锁(w),表级别的意向写锁(w)

3 锁操作应用实例:

多台服务器(保证写入数据主键唯一性多种方案) 
1 使用外部生成的唯一id作为主键,例如(基于uuid生成唯一的id),如果生成规则比较复杂可以提前批量生成不重复的主键id,用到的时候直接从主键集合取值  
2 基于三方的redis作为分布式锁 SETNX key value  

参考文档

mongo锁机制简介