超卖问题:在redis中存在库存inventory为100,如果存在多个线程,多个节点,同时来减库存,则有可能存在少减库存的情况。
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(())
}
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(())
}
实现
缺点
大概实现
确定
所有读数据走读锁,修改数据走写锁,在读多,写少的场景下是可以使用的。
//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,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
canal的工作原理就是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。
目前来看,canal是数据库与缓存一致性的终极解决方案。
未完待续