nosql 四大类
- kv型
- 以redis(远程字典服务),也是本文主要操作的对象
- 文档型,
- MongoDB:基于分布式文件存储,c++编写,处理大量文档,传输给为bson。
- ConthDB,没用过,不知道
- 列存储数据库
- HBase
- Cassandra
- 分布式文件系统
- 图形关系数据库
- Neo4j
- 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(())
}
缓存与数据库双写不一致问题
不靠谱方法一:延迟双删
实现
- 先修改数据库中的内容
- 完成后,删除缓存
- 延时几十ms,再次删除缓存
缺点
- 好似赌博(不解决根本问题)
- 影响请求的响应时间,影响系统吞吐量
不靠谱方法二:消息队列
大概实现
- 为每个key声明一个队列
- 所有对这个key的操作依次放入队列中
- 依次执行队列中的命令
确定
- 光听起来就极为复杂的方案,实现起来不知道要花费多少头发
- 解决了不一致的问题,但预计会引出新的问题
方法三:读写锁
所有读数据走读锁,修改数据走写锁,在读多,写少的场景下是可以使用的。
- 下面代码实现了一个简陋的读写锁,写优先,并且允许设置读写超时,以应对宕机造成的锁无法释放。
- 代码中读锁可以是乐观锁,但写锁一定要设置为悲观锁(读多写少),尝试轮询加锁,无论是否获取锁,都应该在轮询结束后尝试释放锁。
- 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是数据库与缓存一致性的终极解决方案。
未完待续