【mysql系列】锁

本文介绍了数据库中的锁粒度,包括表锁和行锁,详细解析了InnoDB和MyISAM存储引擎的锁支持。讨论了表锁的读写模式以及乐观锁和悲观锁的工作机制、实现方式和适用场景。举例说明了Java中乐观锁的实现,如AtomicInteger的使用,并对比了CAS与synchronized的适用情况。
摘要由CSDN通过智能技术生成

 

目录

从锁的粒度,我们可以分成两大类:

不同的存储引擎支持的锁粒度是不一样的:

表锁下又分为两种模式:

乐观锁悲观锁应用

机制

实现方式

使用场景

实际应用

CAS

CAS与synchronized的使用情景


从锁的粒度,我们可以分成两大类:

  • 表锁
    • 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
  • 行锁
    • 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高

不同的存储引擎支持的锁粒度是不一样的:

  • InnoDB行锁和表锁都支持
  • MyISAM只支持表锁

InnoDB只有通过索引条件检索数据才使用行级锁,否则,InnoDB将使用表锁

  • 也就是说InnoDB的行锁是基于索引的

表锁下又分为两种模式

  • 表读锁(Table Read Lock)
  • 表写锁(Table Write Lock)
  • 从下图可以清晰看到,在表读锁和表写锁的环境下:读读不阻塞,读写阻塞,写写阻塞
    • 读读不阻塞:当前用户在读数据,其他的用户也在读数据,不会加锁
    • 读写阻塞:当前用户在读数据,其他的用户不能修改当前用户读的数据,会加锁!
    • 写写阻塞:当前用户在修改数据,其他的用户不能修改当前用户正在修改的数据,会加锁!

从上面已经看到了:读锁和写锁是互斥的,读写操作是串行

  • 如果某个进程想要获取读锁,同时另外一个进程想要获取写锁。在mysql里边,写锁是优先于读锁的
  • 写锁和读锁优先级的问题是可以通过参数调节的:max_write_lock_countlow-priority-updates

值得注意的是:

  • MyISAM可以支持查询和插入操作的并发进行。可以通过系统变量concurrent_insert来指定哪种模式,在MyISAM中它默认是:如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。
  • 但是InnoDB存储引擎是不支持的

乐观锁悲观锁应用

机制

  1. 悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。

  2. 乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。乐观锁的实现方案一般来说有两种: 版本号机制CAS实现 。乐观锁多适用于多度的应用类型,这样可以提高吞吐量。

实现方式

乐观锁,version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。sql实现代码:

update table 
set x=x+1, version=version+1 
where id=#{id} and version=#{version};
  CAS操作方式:即compare and swap 或者 compare and set,

悲观锁,是由数据库自己实现的,要用的时候,我们直接调用数据库的相关语句就可以了(原理:共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),如行锁、读锁和写锁等,都是在操作之前加锁。

使用场景

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

Java 中的 Synchronized 和 ReentrantLock 等独占锁(排他锁)也是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock 不管是否持有资源,它都会尝试去加锁,生怕自己心爱的宝贝被别人拿走。

实际应用

悲观锁:

一般来说,悲观锁不仅会对写操作加锁还会对读操作加锁,一个典型的悲观锁调用:

 select * from student where name="cxuan" for update

这条 sql 语句从 Student 表中选取 name = "cxuan" 的记录并对其加锁,那么其他写操作再这个事务提交之前都不会对这条数据进行操作,起到了独占和排他的作用。

悲观锁因为对读写都加锁,所以它的性能比较低,对于现在互联网提倡的三高(高性能、高可用、高并发)来说,悲观锁的实现用的越来越少了,但是一般多读的情况下还是需要使用悲观锁的,因为虽然加锁的性能比较低,但是也阻止了像乐观锁一样,遇到写不一致的情况下一直重试的时间。

乐观锁:

乐观锁用于读多写少的情况,即很少发生冲突的场景,这样可以省去锁的开销,增加系统的吞吐量。

乐观锁的适用场景有很多,典型的比如说成本系统,柜员要对一笔金额做修改,为了保证数据的准确性和实效性,使用悲观锁锁住某个数据后,再遇到其他需要修改数据的操作,那么此操作就无法完成金额的修改,对产品来说是灾难性的一刻。

CAS

CAS 即 compare and swap(比较与交换),是一种有名的无锁算法。即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步

CAS 中涉及三个要素:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

仅当 预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做

JAVA对 CAS 的支持:在JDK1.5 中新添加 java.util.concurrent (J.U.C) 就是建立在 CAS 之上的。对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种实现。所以J.U.C在性能上有了很大的提升。

我们以 java.util.concurrent 中的AtomicInteger 为例,看一下在不用锁的情况下是如何保证线程安全的

 public class AtomicCounter {
 ​
     private AtomicInteger integer = new AtomicInteger();
 ​
     public AtomicInteger getInteger() {
         return integer;
     }
 ​
     public void setInteger(AtomicInteger integer) {
         this.integer = integer;
     }
 ​
     public void increment(){
         integer.incrementAndGet();
     }
 ​
     public void decrement(){
         integer.decrementAndGet();
     }
 ​
 }
 ​
 public class AtomicProducer extends Thread{
 ​
     private AtomicCounter atomicCounter;
 ​
     public AtomicProducer(AtomicCounter atomicCounter){
         this.atomicCounter = atomicCounter;
     }
 ​
     @Override
     public void run() {
         for(int j = 0; j < AtomicTest.LOOP; j++) {
             System.out.println("producer : " + atomicCounter.getInteger());
             atomicCounter.increment();
         }
     }
 }
 ​
 public class AtomicConsumer extends Thread{
 ​
     private AtomicCounter atomicCounter;
 ​
     public AtomicConsumer(AtomicCounter atomicCounter){
         this.atomicCounter = atomicCounter;
     }
 ​
     @Override
     public void run() {
         for(int j = 0; j < AtomicTest.LOOP; j++) {
             System.out.println("consumer : " + atomicCounter.getInteger());
             atomicCounter.decrement();
         }
     }
 }
 ​
 public class AtomicTest {
 ​
     final static int LOOP = 10000;
 ​
     public static void main(String[] args) throws InterruptedException {
 ​
         AtomicCounter counter = new AtomicCounter();
         AtomicProducer producer = new AtomicProducer(counter);
         AtomicConsumer consumer = new AtomicConsumer(counter);
 ​
         producer.start();
         consumer.start();
 ​
         producer.join();
         consumer.join();
 ​
         System.out.println(counter.getInteger());
 ​
     }
 }

经测试可得,不管循环多少次最后的结果都是0,也就是多线程并行的情况下,使用 AtomicInteger 可以保证线程安全性。 incrementAndGet 和 decrementAndGet 都是原子性操作。本篇文章暂不探讨它们的实现方式。而普通的消费者生产者无法保证每次都是0,因为

原因是 count -= 1 和 count += 1 都是非原子性操作,它们的执行步骤分为三步:

  • 从内存中读取 count 的值,把它放入寄存器
  • 执行 + 1 或者 - 1 操作
  • 执行完成的结果再复制到内存中

如果要把证它们的原子性,必须进行加锁,使用 Synchronzied 或者 ReentrantLock。

#并发执行1000次加减

import junit.framework.Test;

public class Counter {
    int cnt = 0;
    public int getCnt(){
        return cnt;
    }

    public void setCnt(){
        this.cnt = cnt;
    }
    public void add(){
        cnt += 1;
    }
    public void dec(){
        cnt -= 1;
    }

    public static class Consumer extends Thread{
        Counter counter;

        public Consumer(Counter counter){
            this.counter = counter;
        }

        @Override
        public void run() {
            for(int j = 0; j < Test.LOOP; j++){
                counter.dec();
                System.out.println("consumer is :" + counter.getCnt());
            }
        }
}

    public static class Producer extends Thread{
        Counter counter;
        public Producer(Counter counter){
            this.counter = counter;
        }
        @Override
        public void run() {
            for(int i = 0;i < Test.LOOP;++i){
                counter.add();
                System.out.println("producer is :" + counter.getCnt());
            }

    }
}

    public static class Test{
        final static int LOOP = 1000000;
        public static void main(String[] args) throws InterruptedException{
            Counter counter = new Counter();
            Producer producer = new Producer(counter);
            Consumer consumer = new Consumer(counter);

            producer.start();
            consumer.start();

            producer.join();
            consumer.join();

            System.out.println(counter.getCnt());
        }
    }
}

CAS与synchronized的使用情景

简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)

  • 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  • 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洋气月

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值