数据库锁和高并发系统
数据库锁和高并发系统
满小茂 发表于10个月前
数据库锁和高并发系统
  • 发表于 10个月前
  • 阅读 363
  • 收藏 3
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

1.QPS      

      Web系统开发中,会有一种常见的高并发系统,对系统吞吐量要求很高,一般的管理系统用户访问量不大对高并发要求并不够,如果对用户访问量很大的系统,如电商,搜索引擎等API接口,要求会特别的高。其中QPS(Quest per seconds)会有一个计算公式:

             QPS(TPS)= 并发数/响应时间(单位秒);  

       并发数: 可以理解为单位时间内请求接口的用户数量。 系统能同时处理的request/(事务数),可以理解为网络能同时打开的通道,最直接的是Linux系统,每个进程能开启的网络通道个数,比如默认可以打开1024个网络文件描述符。

      响应时间:  一般取平均响应时间,是每个http请求从请求开始到结束花销的时间,一般取系统各个接口的平均时间。

      举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Web服务器,配置MaxClients为500个(表示服务器的最大连接数目)。那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):20*500/0.1 = 100000 (10万QPS)。

2.高并发系统设计

      现在的web系统基本都是基于关系型数据库的,比如互联网最喜欢的MySql系统,当高并发系统中,最简单的是使用悲观锁的方式,不管是数据库还是代码当中,但是这并不是比较好的解决方式。锁的性能是低效的。

       例子:

      当用户请求涉及数据库值修改时,多个用户同时修改值,可能会造成错误,当使用悲观锁时,每次只允许一个用户修改值,比如商品出货这个操作。

        a.查询商品剩余的数量 : select numbers from goods where good_id=${good_id}

        b.生成订单: insert into orders(user_id,good_id) values('12345',${good_id})

        c.减少商品剩余数量: update goods set numbers=numbers-1 where good_id=${good_id}

       一个商品订单操作可以分为这几步,但是如果在多线程情况下,当商品数量剩余数量为1的时候,多个用户同时下单,同时发现商品用户数量为1,都生成了一条订单,实际上商品只有一个了,造成数据不一致错误。

(1) 队列方案

         将用户的所有请求都放入放入队列中,然后消费者线程从队列中取数据,一个一个的处理,这种方式可以一定程度减少并发度,但是如果用户数量暴增,队列入队数量会远高于出队数量,最后导致系统出错,消费堵塞。

(2) 悲观锁方案

       要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

     我们可以使用命令设置MySQL为非autocommit模式:        

 set autocommit=0;
// 0.开始事务
 begin;
// 1.查询出商品信息
 select numbers from goods where good_id=${good_id} for update;
// 2.根据商品信息生成订单
 insert into orders(user_id,good_id) values('12345',${good_id})
// 3.修改商品剩余数量减少一个
 update goods set numbers=numbers-1 where good_id=${good_id}
// 4.提交事务
 commit;

       上面的begin/commit为事务的开始和结束,因为在前一步我们关闭了mysql的autocommit,所以需要手动控制事务的提交,在这里就不细表了。

        在事务中,只有SELECT ... FOR UPDATE  同一笔数据时会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。

      MySQL select…for update的Row Lock与Table Lock

     使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有明确地指定主键或者索引,MySQL 才会执行Row lock 只锁住被选取的数据),否则MySQL 将会执行Table Lock 将整个数据表单给锁住。

      但是悲观锁并不是适用于任何场景,它也有它存在的一些不足,因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受。

(3)乐观锁方案

              

      为了减少锁竞争,我们可以采用悲观锁的方式来实现高并发操作,为需要高并发访问的数据表建立一个version字段,然后操作。

a.查询商品剩余的数量 : select numbers,version from goods where good_id=#{good_id};

b.生成订单: insert into orders(user_id,good_id) values('12345',#{good_id})

c.减少商品剩余数量: update goods set numbers=numbers-1 where good_id=#{good_id} and version=#{version}

  (4)CAS高并发系统

       Compare And Swap 为了降低锁竞争带来的低效率,我们在高并发系统中应该尽量减少锁的使用,而使用CAS无锁操作.

     独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

        在Java并发包中有这样一个包,java.util.concurrent.atomic,该包是对Java部分数据类型的原子封装,在原有数据类型的基础上,提供了原子性的操作方法,保证了线程安全。下面以AtomicInteger为例,来看一下是如何实现的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
public final int decrementAndGet() {
    for (;;) {
        int current = get();
        int next = current - 1;
        if (compareAndSet(current, next))
            return next;
    }
}

 GCC4.1+版本中支持CAS的原子操作(完整的原子操作可参看 GCC Atomic Builtins

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)

type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)

高并发队列推荐使用Disruptor,CAS无锁队列
另外秒杀抢购业务可以参考这篇博文,写得不错

 https://my.oschina.net/wangjie404/blog/815455

   

共有 人打赏支持
粉丝 64
博文 115
码字总数 117706
×
满小茂
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: