摘要:解决数据库并发问题最核心是保证数据一致,其次是不同场景下选择不同方案使得应用性能,开发成本达尽可能达到最优,在此总结下一些常用的处理方案。
在单机,小量并发的系统,往往采用数据库的机制来解决最为便利,有两种解决方案(乐观锁,悲观锁)
乐观锁
对于读取频繁但更新较少的数据采用乐观锁较为合适,把需要加锁的字段拿来做更新的条件,只要查询出来的数据和更新那一刻的数据一致才更新。这种是在不影响性能情况下处理并发问题,缺陷就是并发时候会出现后来者更新失败,需要程序重试或者让操作者再次操作。
select * from order where id = 1
begin;
update order set value='new_value' where id = 1 and value = @current_value;
commit;
采用EF处理的乐观锁 链接
悲观锁
对于更新频繁的数据采用悲观锁较为合适,利用数据库的排他锁,在查询时候对数据加锁,排他锁只有事务完成或者回滚才会释放,已加锁的数据是不能再加锁,并发时候利用这一特性阻塞后来者,这样更新数据就会一个个完成,缺陷就是当并发量上去了,要把控好事务超时问题。
begin;
select * from goods where id = 1 for update;
update goods set stock = stock - 1 where id = 1;
commit;
使用mysql使用排他锁查询需要注意防止死锁和表级锁带来的性能问题。
- 关于死锁 产生原因以及解决方案
- 只有通过索引条件检索数据,InnoDB才能使用行级锁,否则直接表锁
队列
使用悲观锁情况下,如果大量并发进来会导致一些请求进来后开启事务后一直竞争不到锁最后因事务超时而失败,可以在程序加个内部队列,分批执行,以防过早开启事务导致事务超时。
分布式锁
分布式系统下,多个数据库保证数据一致性的方案需要依靠外力,例如缓存锁或Zookeeper。
redis分布式锁,实际上就是在redis生成一条记录(锁),每次从数据库取数据时候都在redis获取这条记录是否被锁加锁,若数据被加锁,则while循环去获取,直到拿到锁才执行你的逻辑,执行完毕后释放锁,若因意外没法释放锁,就等锁过期。
.net core比较多人用的redis框架StackExchange.Redis
//加锁,直到加锁才成功返回true
Task<bool> LockExtendAsync(
RedisKey key,
RedisValue value,
TimeSpan expiry,
CommandFlags flags = CommandFlags.None);
//释放锁
Task<bool> LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None);
//使用方式
if(db.LockExtendAsync())
{
try
{
// you have the lock do work
}
finally
{
db.LockRelease();
}
}
.net版本的 Zookeeper github链接
关于两者简单的比较
- 性能上 Zookeeper > 缓存
- 可靠性 缓存 > Zookeeper
原因是redis采用内存存储,zk需要持久化
网友评论