redis各种场景下疑难杂症 (rust)

目录

    • nosql 四大类
    • redis 特点
    • 超卖问题
        • 方案一:分布式锁
        • 方案二:lua脚本
    • 缓存与数据库双写不一致问题
        • 不靠谱方法一:延迟双删
        • 不靠谱方法二:消息队列
        • 方法三:读写锁
        • 方法四:canal

nosql 四大类

  • kv型
    1. 以redis(远程字典服务),也是本文主要操作的对象
  • 文档型,
    1. MongoDB:基于分布式文件存储,c++编写,处理大量文档,传输给为bson。
    2. ConthDB,没用过,不知道
  • 列存储数据库
    1. HBase
    2. Cassandra
    3. 分布式文件系统
  • 图形关系数据库
    1. Neo4j
    2. InfoGrid

redis 特点

  • 高速缓存
  • 支持多种数据格式
  • 持久化 rdb aof
  • 支持事务
  • 支持集群

超卖问题

超卖问题:在redis中存在库存inventory为100,如果存在多个线程,多个节点,同时来减库存,则有可能存在少减库存的情况。

方案一:分布式锁

  • 下面是一个简单的案例,一个应用级分布式锁,这样是远远不够的。还应该添加重试,超时等等策略。在java中,redisson框架就帮助我们解决了这些问题。而rust里可以使用tower帮助我们完成这些操作。
  • 基于redis的分布式锁不是很严谨。在多节点模式下,一旦主节点挂掉,会导致锁失效。因为redis主要是ap架构,很难处理这样的问题。如果对分布式锁有强一致性的要求,可以使用zookeeper或者etcd。
  • 分布式锁会影响并发性能,一个较好的解决方案是分布式锁。比如我有10w库存,可以分成10个1w,分别用十把锁来控制,则效率几乎提升十倍。当然分段锁也存在它的问题。
const LOCK_INVENTORY:&'static str = "LOCK_INVENTORY";
const INVENTORY:&'static str = "INVENTORY";

#[tokio::main]
async fn main(){
    let client = Client::open("redis://123.57.130.20/1").expect("连接redis失败");
    let mut conn = client.get_tokio_connection().await.unwrap();
    let request_id = rand::thread_rng().gen_range(100000..999999);
    sub_inventory(request_id,&mut conn).await.unwrap();
}
async fn sub_inventory(request_id:i32,conn:&mut Connection) ->Result<(),Box<dyn Error+ 'static>>{
    //加锁 设置超时时间
    //todo 加锁结束后,开始一个新的任务为锁续命,直到锁释放,获超出限制策略
    redis::cmd("set").arg(LOCK_INVENTORY).arg(request_id).arg("ex").arg(10).arg("nx").query_async::<_,String>( conn).await?;
    //减库存
    let res:Option<i32> = conn.get(INVENTORY).await?;
    let res = if let Some(count) = res {
        conn.set(INVENTORY,count - 1).await
    }else{
        //为空则初始化100个
        conn.set(INVENTORY,100).await
    };
    //删除锁
    let rid:i32 = conn.get(LOCK_INVENTORY).await?;
    if rid == request_id {
        let _:() = conn.del(LOCK_INVENTORY).await?;
    }
    res?;Ok(())
}

方案二:lua脚本

redis嵌入lua脚本执行,脚本是原子性的

const SUB_INVENTORY_LUA_SCRIPT:&'static str = r#"
    local count = redis.call("get","INVENTORY")
    -- call出错后会中断执行,如果不确定一定执行正确 请使用pcall
    if not count then
        return redis.call("set",KEYS[1],100)
    else
        return redis.call("set",KEYS[1],count - ARGV[1])
    end
"#;

函数改造如下:

async fn sub_inventory(conn:&mut Connection) ->Result<(),Box<dyn Error+ 'static>>{
    //todo 缓存以后,可以使用evalsha执行
    redis::cmd("eval").arg(SUB_INVENTORY_LUA_SCRIPT).arg(1).arg(INVENTORY).arg(1).query_async(conn).await?;Ok(())
}


缓存与数据库双写不一致问题

不靠谱方法一:延迟双删

实现

  1. 先修改数据库中的内容
  2. 完成后,删除缓存
  3. 延时几十ms,再次删除缓存

缺点

  • 好似赌博(不解决根本问题)
  • 影响请求的响应时间,影响系统吞吐量

不靠谱方法二:消息队列

大概实现

  1. 为每个key声明一个队列
  2. 所有对这个key的操作依次放入队列中
  3. 依次执行队列中的命令

确定

  • 光听起来就极为复杂的方案,实现起来不知道要花费多少头发
  • 解决了不一致的问题,但预计会引出新的问题

方法三:读写锁

所有读数据走读锁,修改数据走写锁,在读多,写少的场景下是可以使用的。

  • 下面代码实现了一个简陋的读写锁,写优先,并且允许设置读写超时,以应对宕机造成的锁无法释放。
  • 代码中读锁可以是乐观锁,但写锁一定要设置为悲观锁(读多写少),尝试轮询加锁,无论是否获取锁,都应该在轮询结束后尝试释放锁。
  • read:读锁,write:写锁,unread:释放读锁,unwrite:释放写锁
    代码:
//ARGV[1] 请求命令,read:读锁,write:写锁,unread:释放写锁,unwrite:释放读锁
//ARGV[2]:request_id(类型number)  ARGV[3]:当前时间戳 ARGV[4] 超时时间s
const SUB_INVENTORY_LUA_SCRIPT:&'static str = r#"
    if redis.call("exists",KEYS[1]) == 0 then
        redis.call("hset",KEYS[1],"reader",0,"writer",0,"rtimeout",0,"wtimeout",0)
    end
    local lock = redis.call("hmget",KEYS[1],"reader","writer","rtimeout","wtimeout")
    if ARGV[1] == "read" then
        if tonumber(lock[2]) == 0 or tonumber(lock[4]) < tonumber(ARGV[3]) then
            return redis.call("hmset",KEYS[1],"reader",tonumber(lock[1])+1,"rtimeout",tonumber(ARGV[3])+tonumber(ARGV[4]))
        end
    elseif ARGV[1] == "write" then
        if tonumber(lock[2]) == 0 or tonumber(lock[4]) < tonumber(ARGV[3]) or lock[2] == ARGV[2] then
            redis.call("hmset",KEYS[1],"writer",ARGV[2],"wtimeout",tonumber(ARGV[3])+tonumber(ARGV[4]))
            if tonumber(lock[1]) == 0 or tonumber(lock[3]) < tonumber(ARGV[3]) then
                return true
            end
        end
    elseif ARGV[1] == "unread" then
        return redis.call("hincrby",KEYS[1],"reader",-1)
    elseif ARGV[1] == "unwrite" then
        if lock[2] == ARGV[2] then
            return redis.call("hMset",KEYS[1],"reader",0,"writer",0)
        end
    end
    return false
"#;

方法四:canal

canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

canal的工作原理就是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。

目前来看,canal是数据库与缓存一致性的终极解决方案。

未完待续

你可能感兴趣的:(架构,rust,redis,rust,数据库与缓存一致性,分布式锁,缓存)