木子雷

木子雷 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 leishen66.gitee.io/ 编辑
编辑

Web后端码仔,记录生活,分享技术!

公众号 搜索:木子雷
CSDN:https://blog.csdn.net/feichit...
掘 金: https://juejin.im/user/5c67b8...
OSCHINA: https://my.oschina.net/u/4216693
GitHub:https://github.com/leishen6
个人网站:https://leishen66.gitee.io/

个人动态

木子雷 发布了文章 · 3月5日

一文带你了解MySql并发事务中的数据库锁、隔离级别、MVCC

前言:

最近同事开发时遇到了一个事务阻塞的问题,通过网上查询发现关于MySQL事务、锁这一块的资料都比较絮乱,让人看的云里雾里,所以借着这个机会,刚好也对这一块内容做一个总结梳理,希望能比较全面去写一下MySQL的并发事务处理。

本文主线:

  • 简述事务的特性与隔离级别
  • 聊聊MySql中各种类型的锁
  • 然后再聊聊MVCC是个什么东东
  • 接着再聊聊数据库锁的触发及升级,以及死锁
  • 最后说下出现锁问题时的常见排查命令

简述事务的特性与隔离级别:

在讲锁之前,必须要先聊聊 事务的特性与隔离级别 ,因为锁机制的存在是为了保证事务对应隔离级别下的特性。

事务具有以下几个特性:

说完特性,再聊聊MySql中的几种事务隔离级别:

RU 读未提交:

顾名思义,在这种隔离级别下,当多个事务并行对同一数据进行操作时,会读取未提交的数据,也被称之为 脏读

这种隔离级别因为会出现脏读现象,所以在实际场景中很少用。

RC 读提交:

一个事务只能看见已经提交事务所做的改变。

但这种隔离级别会出现 不可重复读 现象,即在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。

RR 可重复读:

这是MySQL的 默认事务隔离级别 ,在这种隔离级别下,解决了RC存在的不可重复读问题,确保在同一事务中,会看到同样的数据行。

但可能会出现 幻读 ,即当一个事务在执行读取操作,第一次查询数据总量后,另一个事务执行了新增数据的操作并提交后,这个时候第一个事务读取的数据总量和之前统计的不一样,就像产生幻觉一样。

SERIALIZABLE 串行化:

此隔离级别是四种隔离级别中最高的级别,解决了 脏读、可重复读、幻读 的问题。

但是性能最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,在并行事务执行过程中,后一个事务的执行必须等待前一个事务结束。

MySql中各种类型的锁:

在MySQL中,按锁类型划分,有以下种类:

提到锁到种类,需要提一下MySQL到存储引擎,MySQL常用引擎有 MyISAM和InnoDB ,而InnoDB是mysql默认的引擎。MyISAM是不支持行锁的,而InnoDB支持行锁和表锁。

MyISAM 存储引擎下表锁:

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁;

读锁会阻塞对同一张表的写操作,而写锁既会阻塞对同一张表的写操作,也会阻塞此表的读操作。

排他锁、共享锁、意向锁 是什么东东?

排他锁:

通常我们在InnoDB存储引擎中对表执行一个更新操作,针对这一行数据会持有排他锁;

持有排他锁时,不允许再在数据行上添加写锁与读锁,其他事务对此行数据的读、写操作都会被阻塞,只有当前事务提交了,锁释放了才允许其他事务进行读写,达到避免 脏读 的效果。

共享锁:

主要是为了支持并发的读取数据而出现的,当一个事务持有某一数据行的共享锁时,允许其它事务可以再获取共享锁,但不允许其它事务在此基础上获取排他锁;

也就是说,在持有共享锁时,多个事务可以同时读取当前数据,但是不允许任何事务同时对当前数据进行修改操作,阻塞添加排它锁。

意向锁:

首先需要明白一点,意向锁的作用是在 表上 的,当一个事务需要获取共享锁或排他锁时,首先要获取对应的意向锁;

为什么要这样做呢?举个例子,假设在事务A中,对某一行数据添加共享锁,这一行只能读,不能写;此时事务B申请获得表的写锁,假如加锁成功,那么事务B将能够对整个表的数据进行读写,与事务A冲突,这种操作肯定是不允许的呢;

所以MySQL会在申请共享锁或者排他锁的时候,先获取对应的意向锁,也就是说,你要操作表中的某一行锁数据,先要看看整个表能不能被操作;意向锁的申请是由数据库完成的,不需要人为申请。

Innodb 存储引擎下的行锁:

上文对几种锁类型进行了简要分析,其实平时工作开发中接触到最多的还是行锁,行锁的实现有以下几种:

注意:在Innodb 存储引擎中,行锁的实现是基于索引的

Record Lock(记录锁):

它是会锁住索引记录,比如 update table where id = 1, id 是主键,然后在聚簇索引上对 id=1 的个索引记录进行加锁;

Gap Lock(间隙锁):

实质上是对索引前后的间隙上锁,不对索引本身上锁,目的是为了防止幻读。

当使用范围条件查询而不是等值条件检索数据,并请求排他锁、或共享锁时,对于该范围内不存在的记录,不允许其修改插入。

举个例子:当表中只有一条id=101的记录,一个事务执行select * from user where user_id > 100 for update;此时另一个事务执行插入一条id=102的数据是会阻塞的,必须等待第一个事务提交后才能完成。

间隙锁是针对事务隔离级别为可重复读或以上级别的。

Next-Key Lock:

Next-Key Lock 是 记录锁和间隙锁 的结合,会同时锁住记录与间隙。

在innodb存储引擎中,如果没有通过 索引项 进行查询时:

①、在RR隔离级别下,会以Next-Key Lock的方式对数据行进行加锁,通过 行锁+间隙锁 实现了 "锁表" 的效果,但请记住这不是添加的表锁;

②、而在 RU、RC 隔离级别下还是只会锁行记录,为什么呢?因为在innodb存储引擎下的四种事务隔离级别中都支持行锁,但是间隙锁只存在于RR、Serializable 两种隔离级别下。

可以通过下面这篇文章了解为什么在RR隔离级别下会实现"锁表"的效果,而在RC隔离级别下只会锁行记录: 互联网项目中mysql应该选什么事务隔离级别

MVCC 是什么:

锁机制可以控制并发操作,来保证一致性,但是系统开销会很大;在RC、RR的隔离级别下,MySQL的InnoDB存储引擎通过 MVCC (多版本并发控制) 机制来解决幻读。

使用MVCC时具体的体现是什么呢?

使事务在并发过程中,SELECT 操作不用加锁,读写不冲突从而提高性能。

那么实现MVCC机制的原理是什么呢?

其原理是通过保存数据在某个时间点的快照来实现的;通过在每行记录后面保存隐藏列来存放事务ID,这样每一个事务,都会对应一个递增的事务ID。

假设三个事务同时更新来同一行数据,那么就会对应三个数据版本,但实际上版本1、版本2并不是物理存在的,而是通过关联记录在undo log 回滚日志中,这样就可以通过undo log找回数据的历史版本,比如回滚的操作,会使用上一个版本的数据覆盖数据页上的数据。

举例一个RR隔离级别下快照读的例子:

开启事务A按条件A查询到两条数据,此时事务B再插入1条数据且满足条件A的数据,并提交事务;

此时事务A再按条件A进行查询,查询到的依然是两条数据,也就是说,事务A查询到的并不是当前最新的数据版本(三条数据),而是通过MVCC实现的历史快照版本;这也是可重复读的实现。

介绍了完读操作,再举例一个RR隔离级别下 更新 写操作的例子:

注意:对数据修改的操作(update、insert、delete)都会读到已提交事务的最新数据,因为这就是 当前读。

假设事务A执行一个更新语句,满足更新条件A(条件A字段无索引或者存在非唯一索引)的数据是2条,更新成功后不提交事务;

此时事务B插入一条新的满足条件A的数据时会被阻塞的,为什么呢?

因为这里在事务A更新时使用到了Next-Key Lock锁,它会使用行锁+间隙锁实现了"锁表",所以后面事务B再进行插入数据时会被阻塞的;这也防止了幻读的出现。

这里如果看的不是很明白的话,可以同时再参考下此文章,此文章详细分析了加锁情况:惊!史上最全的select加锁分析(Mysql)

注意:

MVCC只在RC和RR两个隔离级别下支持;其他两个隔离级别和MVCC不兼容,因为 RU总是读取最新的数据行,而不是符合当前事务版本的数据行;而S ERIALIZABLE 则会对所有读取的行都加锁,是当前读,也是读取的最新数据。

数据库锁的触发及升级,以及死锁:

数据库锁的触发及升级:

什么时候会出现DeadLock:

什么是死锁呢?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

举例说明:

事务A获取 id=20的锁,事务B获取id=30的锁,然后,事务A试图获取id=30的锁,而该锁已经被事务B持有,所以事务A等待事务 B释放该锁,然后事务B又试图获取id =20 的锁这个锁被事务 A 占有,于是两个事务之间相互等待,这就会导致死锁。

死锁的场景还有许多,归根结底,都是因为多个事务想要获取的锁互斥且获取的顺序不一致所造成。

如何避免死锁呢?

通常Record Lock引起的死锁问题开发时都会比较小心,但Gap Lock可能导致死锁的问题通常会被忽略,所以这一点要多加注意,另外就是建立合适的索引,如果没有索引,那么在操作数据时会锁住每一行,会增大死锁的概率。

出现锁问题时的常见排查命令:

show open tabbles:

SHOW OPEN TABLES where In_use > 0:查看哪些表被锁了

show status like 'table%':

  • table_locks_waited:出现表级锁定争用发生等待的次数,此值高说明存在验证的表记锁争用情况
  • table_locks_immediate:表示立即释放表锁的次数

show status like 'innodb_row_lock%':

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量
  • Innodb_row_lock_time:系统启动到现在锁定总时间
  • Innodb_row_lock_time_avg:每次等待话费的平均时间
  • Innodb_row_lock_time_max:系统启动到现在等待最长一次所花时间
  • Innodb_row_lock_waits:系统启动后到现在共等待次数

information_schema:

information_schema是MySQL专门记录性能信息的库,在5.7版本后默认打开。

  • SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS: 查看当前InnoDB的锁的信息,会显示是什么锁类型,属于那个事务ID
  • SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX: 查看InnoDB事务ID,会显示是什么操作和一些常规信息,例如是否在运行running,还是等待锁
  • SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS:查看InnoDB锁的等待时间,和等待的是哪个事务ID的锁

♡ 点赞 + 评论 + 转发 哟

如果本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创作的动力,谢谢啦!

您可以微信搜索 【木子雷】 公众号,大量Java学习干货文章,您可以来瞧一瞧哟!

查看原文

赞 0 收藏 0 评论 0

木子雷 发布了文章 · 2月19日

一文让你了解如何快速、优雅的实现导出Excel

前言:

春节假期刚刚过去,大家是不是已经开始了搬砖生活啦,嘻嘻 o(∩_∩)o ,可我还在休假中呢 !

好啦,咱们言归正传,开始聊聊正文。做过后端管理系统的同学,大概率都会收到过实现 导出Excel 的功能需求,因为这个功能在后台管理系统中是个必备功能。

那大家是怎么实现这个功能的呢?

  • 使用Apache提供POI组件实现;
  • 使用现成的、简便的第三方工具库(避免重复造轮子)

    • Hutool 工具库中的Excel工具类
    • EasyExcel 阿里开源的基于Java的简单、省内存的读写Excel工具库

接下来咱们来聊聊使用 Hutool、EasyExcel 工具库实现导出Excel。

使用第三方库实现导出Excel

业界有句话:不重复造轮子。 使用工具类可以减少日常繁琐的编码,减少重复的编码时间,提高开发效率。 作为程序员,应该多善于利用工具减少代码冗余,美化自己的代码。

使用 Hutool 工具库实现导出Excel:

1、首先添加依赖

在pom.xml中添加上依赖:

<!--hutool 导出 Excel 工具组件-->
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.1.0</version>
</dependency>

<!--POI组件-->
<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi-ooxml</artifactId>
  <version>4.1.0</version>
</dependency>

上面除了引入了 hutool 依赖之外,还引入了 poi-ooxml 依赖,这个包会自动关联引入poi包,且可以很好的支持Office2007+的文档格式 。

2、然后使用工具类实现导出Excel

import cn.hutool.core.collection.CollUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import cn.hutool.poi.excel.StyleSet;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;

import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @PACKAGE_NAME: com.lyl.excel
 * @ClassName: HutoolExcelUtils
 * @Description: 使用 Hutool 中的工具类实现 Excel的导出
 * @Date: 2021-02-18 16:24
 * @Author: [木子雷] 公众号
 **/
public class HutoolExcelUtils {


    /**
     * 导出Excel
     *
     * @param args
     */
    public static void main(String[] args) {

        ArrayList<Map<String, Object>> rows = CollUtil.newArrayList(data());
        ExcelWriter writer = null;

        try {
            String path = "E:/QQPCmgr/Desktop/";

            String excelName = "Hutool" + System.currentTimeMillis() + ".xlsx";
            // 通过工具类创建writer,固定的文件输出路径
            writer = ExcelUtil.getWriter(path + excelName);

            // 定义第一行合并单元格样式
            CellStyle headCellStyle = writer.getHeadCellStyle();
            // 设置内容字体
            Font font = writer.createFont();
            // 字体加粗
            font.setBold(true);
            // 字体颜色
            font.setColor(Font.COLOR_RED);
            headCellStyle.setFont(font);

            // 设置第 0 列的单元格的宽度,列数从零开始计算
            writer.setColumnWidth(0, 20);
            writer.setColumnWidth(1, 20);
            writer.setColumnWidth(2, 20);

            // 定义数据行的样式
            StyleSet style = writer.getStyleSet();
            // 设置单元格文本内容自动换行
            style.setWrapText();

            // 合并单元格后的标题行(第一行),使用默认标题样式
            writer.merge(rows.get(0).size() - 1, "导出测试:TEST");
            // 一次性写出内容,使用默认样式,强制输出标题
            writer.write(rows, true);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                // 记住关闭 writer,释放内存
                writer.close();
            }
        }
    }


    /**
     * 构造 导出的数据
     *
     * @return
     */
    public static List<Map<String, Object>> data() {
        // 导出的数据
        ArrayList<Map<String, Object>> rows = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            Map<String, Object> row = new LinkedHashMap<>();

            row.put("字符串标题", "字符串" + i);
            row.put("日期标题", new Date());
            row.put("数字标题", 0.56);
            rows.add(row);
        }
        return rows;
    }

}

注意:

  • 记得修改代码中导出Excel的路径 path
  • 导出Excel的样式是可以灵活变化的,可以自行进行设置

3、导出Excel的样式如下

4、注意事项

  • 在导出大数据量时,可能会出现内存溢出的问题,不要担心,Hutool也为我们提供了 BigExcelWriter 来避免大数据量输出时可能会出现的内存溢出问题。
  • 上面的例子中,只实现了部分的样式设置,Hutool还提供了许多其它的样式,大家可以自行去尝试;并且 Hutool 也支持写出到 Web客户端下载

官方文档地址:Hutool 操作 Excel

使用 EasyExcel 工具库实现导出Excel:

 EasyExcel是一个基于Java的简单、省内存的读写Excel的 阿里开源 项目;正如它在GitHub中项目代码介绍的那样:一个快速、简单避免OOM的java处理Excel工具。

测试得知 64M内存1分钟内读取75M(46W行25列)的Excel ;除此之外还有 急速模式 能更快,但是内存占用会在100M多一点 。

1、首先添加依赖

<!--  阿里开源的 excel 工具类库 -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>2.2.6</version>
</dependency>

注意: 此依赖不能和 poi-ooxml 依赖在一起用,否则运行时会报 类找不到 的异常。

2、然后使用工具类实现导出Excel

①、导出的数据模版类:
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentFontStyle;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.ContentStyle;
import com.alibaba.excel.annotation.write.style.HeadFontStyle;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import com.alibaba.excel.annotation.write.style.HeadStyle;
import org.apache.poi.ss.usermodel.FillPatternType;

import java.util.Date;

/**
 * @PACKAGE_NAME: com.lyl.excel
 * @ClassName: DemoData
 * @Description:  使用 EasyExcel 导出数据时的数据模版
 * @Date: 2021-01-27 17:46
 * @Author: [木子雷] 公众号
 **/

// 标题行 背景设置成红色 IndexedColors.RED.getIndex()
@HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 10)
// 标题行 字体设置成20
@HeadFontStyle(fontHeightInPoints = 20)
@ContentRowHeight(25)// 文本内容行的高度
@HeadRowHeight(30)// 标题行的高度
@ColumnWidth(20)// 全局的列宽
public class DemoData {

    // 字符串的列内容 背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
    @ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 40)
    // 字符串的列内容 字体设置成20
    @ContentFontStyle(fontHeightInPoints = 20)
    @ExcelProperty({"导出测试:TEST", "字符串标题"})
    private String string;

    @ColumnWidth(30)
    @ExcelProperty({"导出测试:TEST", "日期标题"})
    private Date date;

    @ExcelProperty({"导出测试:TEST", "数字标题"})
    private Double doubleData;

    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String ignore;

    public String getString() {
        return string;
    }

    public void setString(String string) {
        this.string = string;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Double getDoubleData() {
        return doubleData;
    }

    public void setDoubleData(Double doubleData) {
        this.doubleData = doubleData;
    }

    public String getIgnore() {
        return ignore;
    }

    public void setIgnore(String ignore) {
        this.ignore = ignore;
    }
}

注意: 这个数据模版类中使用了大量的 自定义注解 ,通过使用注解可以使代码变得更加的优雅、简洁 。

关于项目中自定义注解的实际使用可以参考:自定义注解的魅力你到底懂不懂

②、实现数据导出到Excel:
import com.alibaba.excel.EasyExcel;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @PACKAGE_NAME: com.lyl.excel
 * @ClassName: EasyExcelUtils
 * @Description: 阿里巴巴 开源的  easyexcel 工具
 * @Date: 2021-01-20 16:58
 * @Author: [木子雷] 公众号
 **/
public class EasyExcelUtils {


    public static void main(String[] args) {
        // 导出Excel的路径
        String path = "E:/QQPCmgr/Desktop/";

        // 导出Excel路径 + 文件名称
        String filename = path + "EasyExcel" + System.currentTimeMillis() + ".xlsx";

        /**
         * 导出excel
         * filename:导出excel全路径
         * DemoData.class:导出excel时的 数据模版
         * 模板:指的是导出excel的sheet页
         * data():构造的DemoData.class数据模版的数据集合 
         */
        EasyExcel.write(filename, DemoData.class).sheet("模板").doWrite(data());
    }


    /**
     * 构造 导出的数据
     *
     * @return
     */
    private static List<DemoData> data() {
        List<DemoData> list = new ArrayList<DemoData>();
        for (int i = 0; i < 10; i++) {
            DemoData data = new DemoData();
            data.setString("字符串" + i);
            data.setDate(new Date());
            data.setDoubleData(0.56);
            list.add(data);
        }
        return list;
    }
}

注意:

  • 记得修改代码中导出Excel的路径 path
  • 导出Excel的样式是可以灵活变化的,可以自行进行设置

3、导出Excel的样式如下

4、EasyExcel 导出Excel扩展:

上面导出Excel的例子中,只实现了其中一部分功能,还有很多功能由于篇幅原因没法一一展示出来,在这就简单说下支持的其它功能:

  • 通过设置 只导出模版数据中的指定列数据
  • 通过设置 将模版数据中的列数据导出到Excel中指定的列上
  • 可以将导出的数据分多个批次导入到同一个Excel中,避免大数据量时的内存溢出问题
  • 导出数据的自定义格式转换,例如:日志、数字的格式转换等
  • 支持将图片导出到Excel中
  • 支持根据已有的Excel模版样式 将数据导出Excel
  • 支持单元格合并、表格方式导出、自动列宽、设置单元格下拉、超链接等、以及插入批注
  • 除了上面的导出Excel功能之外,EasyExcel还支持 读取Excel数据、Web客户端的上传、下载等

官方文档地址:阿里开源 EasyExcel

项目代码地址:alibaba/easyexcel

EasyExcel项目代码拉取下来后,可以直接去单元测试包下,查看已提供的功能测试使用方法:

♡ 点赞 + 评论 + 转发 哟

如果本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创作的动力,谢谢啦!

您可以微信搜索 【木子雷】 公众号,大量Java学习干货文章,您可以来瞧一瞧哟!

查看原文

赞 0 收藏 0 评论 0

木子雷 回答了问题 · 1月27日

关于MySQL索引的index_merge

mysql 的索引合并:

mysql 在 5.0版本开始引入了 “索引合并”策略,可以在一定程度上上使用表中的多个 单列索引 来进行数据的检索 。

正如上文中表中创建了 deleteTime和isFinish两个单例索引;但是在 select * from table where deleteTime=xxx and isFinish=xxx 这个语句执行中,任何一个单列索引都不是最好的选择,这时就可能会使用 索引合并。

需要注意的是:索引合并过程中需要耗费大量的CPU和内存等资源进行索引合并的计算,但是这个合并索引 是通过计算出来的,到底还是不如真实的联合索引,所以可能在过程中扫描的数据行比走联合索引扫描的数据行多,但是比走单列索引是扫描的数据行少。

注意:优化器并不会把索引合并的计算成本算在整个查询成本中,所以在执行计划中出现了 索引合并 时,需要记住此时 实际查询成本 要比 执行计划中的成本大很多;所以在执行计划中出现索引合并时 就需要进行优化了。

关注 2 回答 1

木子雷 发布了文章 · 1月19日

不能错过的CAS+volatile实现同步代码块

前言:

最近看到有人说可以使用 CAS + volatile 实现同步代码块。

心想,确实是可以实现的呀!因为 AbstractQueuedSynchronizer(简称 AQS)内部就是通过 CAS + volatile(修饰同步标志位state) 实现的同步代码块。

并且ReentrantLock就是基于AQS原理来实现同步代码块的;ReentrantLock源码学习和了解AQS原理可以参考:带你探索ReentrantLock源码的快乐

今天,咱们就通过 CAS + volatile 实现一个 迷你版的AQS ;通过这个迷你版的AQS可以使大家对AQS原理更加清晰。

本文主线:

  • CAS操作和volatile简述
  • CAS + volatile = 同步代码块(代码实现)

CAS操作和volatile简述:

通过了解CAS操作和volatile来聊聊为什么使用它们实现同步代码块。

CAS操作:

CAS是什么?

CAS是compare and swap的缩写,从字面上理解就是比较并更新;主要是通过 处理器的指令 来保证操作的原子性 。

CAS 操作包含三个操作数:

  • 内存位置(V)
  • 预期原值(A)
  • 更新值(B)

简单来说:从内存位置V上取到存储的值,将值和预期值A进行比较,如果值和预期值A的结果相等,那么我们就把新值B更新到内存位置V上,如果不相等,那么就重复上述操作直到成功为止。

例如:JDK中的 unsafe 类中的 compareAndSwapInt 方法:

unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  • stateOffset 变量值在内存中存放的位置;
  • expect 期望值;
  • update 更新值;

CAS的优点:

CAS是一种无锁化编程,是一种非阻塞的轻量级的乐观锁;相比于synchronized阻塞式的重量级的悲观锁来说,性能会好很多 。

但是注意:synchronized关键字在不断的优化下(锁升级优化等),性能也变得十分的好。

volatile 关键字:

volatile是什么?

volatile是java虚拟机提供的一种轻量级同步机制。

volatile的作用:

  • 可以保证被volatile修饰的变量的读写具有原子性,不保证复合操作(i++操作等)的原子性;
  • 禁止指令重排序;
  • 被volatile修饰的的变量修改后,可以马上被其它线程感知到,保证可见性;
通过了解CAS操作和volatile关键字后,才可以更加清晰的理解下面实现的同步代码的demo程序。

CAS + volatile = 同步代码块

总述同步代码块的实现原理:

  1. 使用 volatile 关键字修饰一个int类型的同步标志位state,初始值为0;
  2. 加锁/释放锁时使用CAS操作对同步标志位state进行更新;

    • 加锁成功,同步标志位值为 1,加锁状态;
    • 释放锁成功,同步标志位值为0,初始状态;

加锁实现:

加锁流程图:

加锁代码:

 **
 * 加锁,非公平方式获取锁
 */
public final void lock() {
  
    while (true) {
        // CAS操作更新同步标志位
        if (compareAndSetState(0, 1)) {
            // 将独占锁的拥有者设置为当前线程
            exclusiveOwnerThread = Thread.currentThread();

            System.out.println(Thread.currentThread() + "  lock success ! set lock owner is current thread .  " +
                    "state:" + state);

            try {
                // 睡眠一小会,模拟更加好的效果
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 跳出循环
            break;

        } else {
            // TODO 如果同步标志位是1,并且锁的拥有者是当前线程的话,则可以设置重入,但本方法暂未实现
            if (1 == state && Thread.currentThread() == exclusiveOwnerThread) {
                // 进行设置重入锁
            }

            System.out.println(Thread.currentThread() + "  lock fail ! If the owner of the lock is the current thread," +
                    "  the reentrant lock needs to be set;else Adds the current thread to the blocking queue .");

            // 将线程阻塞,并将其放入阻塞列表
            parkThreadList.add(Thread.currentThread());
            LockSupport.park(this);

            // 线程被唤醒后会执行此处,并且继续执行此 while 循环
            System.out.println(Thread.currentThread() + "  The currently blocking thread is awakened !");
        }
    }
}

锁释放实现:

释放锁流程图:

释放锁代码:

/**
 * 释放锁
 *
 * @return
 */
public final boolean unlock() {
    // 判断锁的拥有者是否为当前线程
    if (Thread.currentThread() != exclusiveOwnerThread) {
        throw new IllegalMonitorStateException("Lock release failed !  The owner of the lock is not " +
                "the current thread.");
    }
    // 将同步标志位设置为0,初始未加锁状态
    state = 0;
    // 将独占锁的拥有者设置为 null
    exclusiveOwnerThread = null;

    System.out.println(Thread.currentThread() + "  Release the lock successfully, and then wake up " +
            "the thread node in the blocking queue !  state:" + state);

    if (parkThreadList.size() > 0) {
        // 从阻塞列表中获取阻塞的线程
        Thread thread = parkThreadList.get(0);
        // 唤醒阻塞的线程
        LockSupport.unpark(thread);
        // 将唤醒的线程从阻塞列表中移除
        parkThreadList.remove(0);
    }

    return true;
}

完整代码如下:

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;


/**
 * @PACKAGE_NAME: com.lyl.thread6
 * @ClassName: AqsUtil
 * @Description: 使用 CAS + volatile 同步标志位  =  实现 迷你版AQS ;
 * <p>
 * <p>
 * 注意:本类只简单实现了基本的非公平方式的独占锁的获取与释放; 像重入锁、公平方式获取锁、共享锁等都暂未实现
 * <p/>
 * @Date: 2021-01-15 10:55
 * @Author: [ 木子雷 ] 公众号
 **/
public class AqsUtil {

    /**
     * 同步标志位
     */
    private volatile int state = 0;

    /**
     * 独占锁拥有者
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * JDK中的rt.jar中的Unsafe类提供了硬件级别的原子性操作
     */
    private static final Unsafe unsafe;

    /**
     * 存放阻塞线程的列表
     */
    private static List<Thread> parkThreadList = new ArrayList<>();

    /**
     * 同步标志位 的“起始地址”偏移量
     */
    private static final long stateOffset;


    static {
        try {
            unsafe = getUnsafe();
            // 获取 同步标志位status 的“起始地址”偏移量
            stateOffset = unsafe.objectFieldOffset(AqsUtil.class.getDeclaredField("state"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }


    /**
     * 通过反射 获取 Unsafe 对象
     *
     * @return
     */
    private static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            return null;
        }
    }


    /**
     * 加锁,非公平方式获取锁
     */
    public final void lock() {
        
        while (true) {
          
            if (compareAndSetState(0, 1)) {
                // 将独占锁的拥有者设置为当前线程
                exclusiveOwnerThread = Thread.currentThread();

                System.out.println(Thread.currentThread() + "  lock success ! set lock owner is current thread .  " +
                        "state:" + state);

                try {
                    // 睡眠一小会,模拟更加好的效果
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 跳出循环
                break;

            } else {
                // TODO 如果同步标志位是1,并且锁的拥有者是当前线程的话,则可以设置重入,但本方法暂未实现
                if (1 == state && Thread.currentThread() == exclusiveOwnerThread) {
                    // 进行设置重入锁
                }

                System.out.println(Thread.currentThread() + "  lock fail ! If the owner of the lock is the current thread," +
                        "  the reentrant lock needs to be set;else Adds the current thread to the blocking queue .");

                // 将线程阻塞,并将其放入阻塞队列
                parkThreadList.add(Thread.currentThread());
                LockSupport.park(this);

                // 线程被唤醒后会执行此处,并且继续执行此 while 循环
                System.out.println(Thread.currentThread() + "  The currently blocking thread is awakened !");
            }
        }
    }


    /**
     * 释放锁
     *
     * @return
     */
    public final boolean unlock() {
        if (Thread.currentThread() != exclusiveOwnerThread) {
            throw new IllegalMonitorStateException("Lock release failed !  The owner of the lock is not " +
                    "the current thread.");
        }
        // 将同步标志位设置为0,初始未加锁状态
        state = 0;
        // 将独占锁的拥有者设置为 null
        exclusiveOwnerThread = null;

        System.out.println(Thread.currentThread() + "  Release the lock successfully, and then wake up " +
                "the thread node in the blocking queue !  state:" + state);

        if (parkThreadList.size() > 0) {
            // 从阻塞列表中获取阻塞的线程
            Thread thread = parkThreadList.get(0);
            // 唤醒阻塞的线程
            LockSupport.unpark(thread);
            // 将唤醒的线程从阻塞列表中移除
            parkThreadList.remove(0);
        }

        return true;
    }


    /**
     * 使用CAS 安全的更新 同步标志位
     *
     * @param expect
     * @param update
     * @return
     */
    public final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

}

测试运行:

测试代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @PACKAGE_NAME: com.lyl.thread6
 * @ClassName: SynCodeBlock
 * @Description: 简单的测试
 * @Date: 2021-01-15 10:26
 * @Author: [ 木子雷 ] 公众号
 **/
public class SynCodeBlock {


    public static void main(String[] args) {
        // 10 个线程的固定线程池
        ExecutorService logWorkerThreadPool = Executors.newFixedThreadPool(10);

        AqsUtil aqsUtil = new AqsUtil();

        int i = 10;
        while (i > 0) {
            logWorkerThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    test(aqsUtil);
                }
            });
            --i;
        }
    }


    public static void test(AqsUtil aqsUtil) {

        // 加锁
        aqsUtil.lock();

        try {
            System.out.println("正常的业务处理");
        } finally {
            // 释放锁
            aqsUtil.unlock();
        }
    }

}

运行结果:

例如上面测试程序启动了10个线程同时执行同步代码块,可能此时只有线程 thread-2 获取到了锁,其余线程由于没有获取到锁被阻塞进入到了阻塞列表中;

当获取锁的线程释放了锁后,会唤醒阻塞列表中的线程,并且是按照进入列表的顺序被唤醒;此时被唤醒的线程会再次去尝试获取锁,如果此时有新线程同时尝试获取锁,那么此时也存在竞争了,这就是非公平方式抢占锁(不会按照申请锁的顺序获取锁)。

扩展:

上面的代码中没有实现线程自旋操作,下面看看该怎么实现呢?

首先说说为什么需要自旋操作:

因为在某些场景下,同步资源的锁定时间很短,如果没有获取到锁的线程,为了这点时间就进行阻塞的话,就有些得不偿失了;因为进入阻塞时会进行线程上下文的切换,这个消耗是很大的;

使线程进行自旋的话就很大可能会避免阻塞时的线程上下文切换的消耗;并且一般情况下都会设置一个线程自旋的次数,超过这个次数后,线程还未获取到锁的话,也要将其阻塞了,防止线程一直自旋下去白白浪费CPU资源。

代码如下:

❤ 点赞 + 评论 + 转发 哟

如果本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创作的动力,谢谢啦!

您可以VX搜索 【木子雷】 公众号,大量Java学习干货文章,您可以来瞧一瞧哟!

查看原文

赞 0 收藏 0 评论 0

木子雷 发布了文章 · 1月12日

一文带你了解数据库隔离级别和锁之间的联系

前言:

很多人在学习数据库知识时,往往在学习到隔离级别和数据库锁这里有一些疑问:

隔离级别和数据库锁之间有什么关联呢?

本文就和大家聊聊这两者之间的联系,希望对大家有帮助!

聊聊两者之间的联系:

在具体聊之前,咱先记住一句话: 数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,锁的应用最终导致不同事务的隔离级别。

首先来了解下有哪四种隔离级别:

  • 读未提交:(Read Uncommitted)
  • 读已提交(Read Committed) 大多数数据库默认的隔离级别
  • 重复读(Repeatable-Read) mysql数据库所默认的级别
  • 序列化(serializable)

四种隔离级别具体实现和不同:

首先程序是可以并发执行的,同样,在MySQL中,一个表可以由两个或多个进程同时来读写数据,这是没有问题的。

比如,此时有两个进程来读数据,这也没什么问题,允许。但是如果一个进程在读某一行的数据的过程中,另一个在进程又往这一行里面写数据(改、删),那结果会是如何?

同样,如果两个进程都同时对某一行数据进行更改,以谁的更改为准?那结果又会怎样,不敢想象,是不是数据就被破坏掉了。所以此时是冲突的。

既然会冲突就要想办法解决,靠谁来解决,这时候就是靠 锁机制 来维护了。怎么使用锁来使它们不冲突呢?

在事务开始的时候可以给要准备写操作的这一行数据加一个排它锁,如果是读操作,就给该行数据一个读锁。这样之后,在修改该行数据的时候,不让其他进程对该行数据有任何操作。

而读该行数据的时候,其他进程不能更改,但可以读。读或写完成时,释放锁,最后commit提交。 这时候读写就分离开了,写和写也就分离开了。

mysql开发者给这个解决冲突的方案起了一个名字叫做: 读未提交 :(Read Uncommitted)。这也就是事务的第一个隔离性。

注意:上面的加锁和释放锁的过程由mysql数据库自身来维护,不需要我们人为干涉。

但是这个程度的隔离性仅仅是不够的。看下面的测试结果:

1、A修改事务级别为:未提交读。并开始事务,对user表做一次查询:

2、B事务更新一条记录:

3、此时B事务还未提交,A在事务内做一次查询,发现查询结果已经改变:

4、B进行事务回滚:

5、A再做一次查询,查询结果又变回去了:

由试验得知:在一个进程的事务当中,我更改了其中的一行数据,但是我修改完之后就释放了锁,这时候另一个进程读取了该数据,此时先前的事务是还未提交的,直到我回滚了数据,另一个进程读的数据就变成了无用的或者是错误的数据。

我们通常把这种数据叫做脏数据,这种情况读出来的数据叫做 賍读

怎么办呢?当然还是靠锁机制。

无非是锁的位置不同而已,之前是只要操作完该数据就立马释放掉锁,现在是把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的,包括任何操作。

那么数据库为此种状态的数据库操作规则又给了一个名字叫做: 读已提交 (Read Committed),或者也可以叫不可重复读。这也就是事务的第二个隔离性。

在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……

继续看下面的测试结果:

1、把隔离性调为READ-COMMITTED(读取提交内容)设置A的事务隔离级别,并进入事务做一次查询:

2、B开始事务,并对记录进行修改:

3、A再对user表进行查询,发现记录没有受到影响:

4、B提交事务:

5、A再对user表查询,发现记录被修改:

试验进行到这里,你会发现,在同一个事务中如果两次读取相同的数据时,最后的结果却不一致。

这里我们把这种现象称为: 不可重复读 。因为在第一个事务读取了数据之后,此时另一个事务把该数据给修改了,这时候事务提交,那么另一个事务在第二次读取的时候,结果就不一样,一个修改前的,一个是修改后的。

但是细心的你会发现,既然你说此种隔离性是在事务提交后才释放锁,那么在试验过程中,在该数据未提交前,另一个事务为什么也是仍然可以读取的呀。

是上面测试错了吗?不是的;

在这里mysql使用了一个并发版本控制机制,他们把它叫做MVCC,通俗的也就是说:mysql为了提高系统的并发量,在事务未提交前,虽然事务内操作的数据是锁定状态,但是另一个事务仍然可以读取数据的 快照版本 ;像 Oracle等大多数数据库默认的就是 读提交 这个级别的隔离性的。

但是mysql 的默认隔离级别却不是这个 。

而且不只是像上面在更新数据时出现这个问题,在插入数据时仍然会造成类似的这样一种现象: mysql虽然锁住了正在操作的数据行,但它仍然不会阻止另一个事务往表插入新行新的数据。

比如:一个事务读取或更新了表里的所有行,接者又有另一个事务往该表里插入一个新行,在事务提交后;

原来读取或更改过数据的事务又第二次读取了相同的数据,这时候这个事务中两次读取的结果集行数就不一样。原来更新了所有行,而现在读出来发现竟然还有一行没有更新。这就是所谓的 幻读

为了防止在同一事务中两次读取数据不一致,(包括不可重读和幻读),接下来该如何继续做呢?

mysql依然采取的是MVCC并发版本控制来解决这个问题,还是读取的快照数据。

具体是:如果事务中存在多次读取同样的数据,MySQL第一次读的时候仍然会保持选择读最新提交事务的数据,当第一次之后,之后再读时,mysql会取第一次读取的数据作为结果。

这样就保证了同一个事务多次读取数据时数据的一致性。这时候,mysql把这种解决方案叫做: 可重复读 (Repeatable-Read),也就是上述所写的第三个隔离性,也是mysql默认的隔离级别。

注意: 在可重复读的隔离级别下,除了会保证读操作的一致性外,在更新操作(当前读)时也会保证数据的一致性,避免出现不可重复读和幻读的错误;

具体是:在MySql的可重复读的隔离级别下,一个事务中更新数据,并且事务不提交,另启一个事务insert插入新数据,这时候是无法插入新数据的,插入操作被阻塞;为什么被阻塞呢?

因为在第一个事务中进行更新数据时(当前读),会使用 行锁 + 间隙锁 将表锁住了,所以在第二个事务中插入操作被阻塞;只有当第一个事务提交后,第二个事务中的插入操作才能被执行;

如果不阻塞的话,会存在什么问题? 会存在 幻读 的问题;例如:本来将全表数据的某个字段全部进行了更新,但是由于后面新增了数据,这些新增数据的那个字段并没有被更新,再次查看时就跟出现了幻觉一样;所以为了解决幻读的问题, 在可重复读隔离级别中提供了间隙锁 ,使用行锁+间隙锁将表锁住,让所有操作都不能修改数据 。

注意:innodb存储引擎中有对锁等待超时时间的配置,在规定时间内没有获取到锁的话,则此事务被中断,需要应用代码进行手动回滚或重试;

注意:幻读和不可重复读的区别:

  • 幻读是针对的一批数据记录整体
  • 不可重复读针对的是同一数据项的记录
最后这个时候,该我们的最后一种隔离级别也是最高的隔离级:别序列化(serializable)登场了。

该隔离级别会自动在锁住你要操作的整个表的数据,如果另一个进程事务想要操作表里的任何数据就需要等待获得锁的进程操作完成释放锁。

可避免脏读、不可重复读、幻读的发生。当然性能会下降很多,会导致很多的进程相互排队竞争锁。

后记:

上面所说的四种隔离性的锁机制应用是数据库自动完成的,不需要人为干预。

隔离级别的设置只对当前会话连接有效。对于使用MySQL命令窗口而言,一个窗口就相当于一个链接,当前窗口设置的隔离级别只对当前窗口中的事务有效 。

❤ 点赞 + 评论 + 转发 哟

如果本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创作的动力,谢谢啦!

您可以VX搜索 【木子雷】 公众号,大量Java学习干货文章,您可以来瞧一瞧哟!

查看原文

赞 0 收藏 0 评论 0

木子雷 回答了问题 · 1月9日

mysql加了行锁后为什么还能加表读锁?不冲突吗?

1、首先询问下你测试时使用的是MySql 默认的 RR可重复读 隔离级别吗? 如果是,则看下面:

2、你在第一个事务中,你使用 for update 当前读对 id=21 这行记录加上了行锁;并且没有提交事务;

3、然后在第二个事务中使用 lock table student read 对student表 "加表锁" ,注意 加表锁 这三个字被加上了引号,因为其实不是直接加的表锁,而只是实现 锁表 而已; innodb 存储引擎在 RR 隔离级别下是使用 Next-Key Locks 实现锁表的; 也可以理解为是用了行锁+间隙锁来实现锁表的操作!

所以,这也就可以解释为什么在对表中某行记录加上了行锁后,并且在行锁未释放时,也可以直接锁住整张表了。

4、具体可参考这篇文章:惊!史上最全的select加锁分析(Mysql),拿它去怒怼面试官,走起!

希望对你有帮助,谢谢!

关注 2 回答 1

木子雷 赞了文章 · 1月6日

谁说明天上线,这货压根不知道开发流程!


作者:小傅哥

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

互联网公司常见工种有哪些?

互联网中一个项目的上线会需要各个工种间的配合,以研发为视角上会承接产品需求,下会交给测试验证,最终完成项目交付上线。其实除此之外,还会有业务、运营、UI设计、运维,来配合项目的发起、使用和运维维护。

图 18-1,互联网工种协同合作。

图 18-1 互联网工种协同合作

除了一条线上的工作交替配合,还有同工种间的跨部门协同工作。 比如:

  • 产品阶段:A产品中的部分服务,需要由另外一个部门配合开发相关服务支撑。那么双方产品需要协调好时间节奏,配合上线。
  • 研发阶段:承接着产品跨部门的对接功能,双方研发会定义好对接接口、对接时间,以及最终的联调上线。
  • 测试阶段:按照产品的功能节点、研发的开发流程以及接口描述,进行测试验证。

最终,同部门工作的交替、跨部门的工作协同,保障项目开发过程所需的各项物料都如期上线。

接下来我们来说一说,项目上线中各个阶段的执行过程。 当然,并不一定所有的开发都是按照这个过程执行。​会根据公司的体量、项目的大小、架构的模式有些许差异。所以,仅作为参考学习即可,不需要强制趋同。

二、时间节奏

图 18-2 定义时间节奏

  • 级别:⭐⭐⭐⭐
  • 事项:定义项目开发时间节点
  • 人员:业务、产品、研发组长、测试组长、架构师、核心项目成员
  • 描述:这个时间节奏的定义非常重要,它可以是项目经理发起也可以是产品发起。一般很多时候互联公司发一个项目,经常会听到老板说我要这个时间上。可能这句话看上去很不合理,但为了活下去,为了快速站住市场,压到下面执行人员就是一个必须要上线的时间。,这个上线的时间如果想满足, 那么就需要把整体的时间节奏确认出来。比如业务和产品什么时候把需求确认清楚什么时间与研发过PRD研发什么时候开发到提测测试什么时间测试完成如果,没有这个时间节奏,前面的职责人员把时间都耗费没了以后,越往后面风险越高。就像最后研发只有4天,测试只有2天,那带BUG上线吗!?所以要整体把控才是对项目的负责。

三、资源投入

图 18-3 安排资源投入

  • 级别:⭐⭐⭐
  • 事项:研发资源投入
  • 人员:架构师、研发人员、测试人员
  • 描述:站在研发视角,研发需要从工程开发、配合测试(改bug)、项目上线等的全流程参与,是一个较长周期的工作。但在某个阶段所投入的时间成本会有差异,可以按照一定的资源占比进行投入(1是100%、0.8是80%)。那么,当一个新的项目下来以后,就需要按照最近原则和项目的人员可投入情况,进行资源投入安排。如果项目较多的情况下,资源安排不合理。可能会导致项目提交测试晚或某些功能全部由一个研发提交测试的,最终改不过来BUG。从而也就导致了,项目的延期风险。

四、研发、测试、上线阶段

图 18-4 研发、测试、上线阶段

  • 级别:⭐⭐⭐⭐
  • 事项:研发、测试、上线阶段
  • 人员:研发人员、测试人员、架构师/技术组长
  • 描述:这个阶段包括的内容较多,主要是以研发视角看上下衔接人员。研发接过产品的需求开始做设计,设计完成后由研发主导发起设计评审,这个阶段参与的人员较多(研发、架构师、测试、产品等)。功能的合理设计也是可以非常有效的保障资源使用的重要一环,另外一个需求的合理架构将会为后续需求迭代做好铺垫。就像女厕改男厕,如果没有流出小便的水管,就很麻烦。 最终研发完成需要提交相应的成果物,尤其是提测报告、接口文档、单测信息。如果研发不能有完整的单元测试覆盖度,那么交给测试以后,日常的修复bug的事情就会非常多。当研发和测试工作完成以后,接下来就是发布上线。上线前夕会有研发发起上线报告,同时各方配合以及产品、运用准备相应的线上配置数据和权限。最终上线完成交付产品运营使用。

五、项目复盘

图 18-5 项目复盘

  • 级别:⭐⭐⭐⭐
  • 事项:项目复盘
  • 人员:面向研发和测试人员
  • 描述:复盘可能会因为出现事故、技术总结、分享成长,几个方向而进行的归纳、总结,避免同类事情的发生。复盘内容一般会包括技术方面的使用,例如:DB、应用开发、网关等,也包括业务领域逻辑的建设。
  • 复盘DB:

    1. 数据库连接数配置依照业务场景申请增加
  1. 禁止使用复杂嵌套和函数类等做业务查询
  2. 防重逻辑字段加强避免造成不能防重问题
  3. 索引字段初始化检测以及慢查询问题优化
  • 复盘业务:

    1. 对于所有营销类场景的设计需符合标准流程,缓存使用的一致性问题
    1. 资金流水结算方面在防重复设计上加强验证,测试环境模拟多样场景
    2. 对于外部支撑系统的依赖按照业务体量发展,进行通知压测报告流量
    3. 所有核心功能流程加强研发侧代码评审质量,并不断按照发展量优化
    4. 研发侧代码质量提升定期复盘问题以及优化,通过锻炼不断加强质量
    5. 在研发提测、修复、上线流程注意开发分支,避免错乱合并产生问题
    6. 所有的业务流程配置监控与图表并打印日志,方便及时追踪线上异常
    7. 核心场景的全链路压测可以有效的保证质量,也可很好降低流量风险
  • 复盘功能:

    1. 功能逻辑封装优化,缓存、线程、验证
    2. 日志完整性校验,入参、出参、异常
    3. 调用外部接口的超时时长设定以及重试约定
    4. 异常展示的紧急问题,测试环境复现追溯
  • 复盘部署:

    1. 按照压测标准部署服务
    2. 核心业务双机房三机房
    3. 非核心业务隔离RPC接口配置
    4. 按需调整JVM、连接数、日志等参数
  • 复盘接口:

    1. 功能验证的完整性
    2. 异常流程的复测性
    3. 数据指标监控范围
    4. 新上线后定期检测
  • 综上,可能仅仅是对某一次项目的总结性复盘,便于新人接受和理解项目的重点内容。如果团队中能及时有效的汇总技术并落地资料,可以非常有效的做好技术传承。

    六、总结

    • 互联网中一般中大型项目的开发过程,涉及的流程一般较多,也需要合理的把控。否则可能会出现一些过程中的风险,导致项目不能如期上线。当然也并不是所有项目都需要这样处理,例如一些小功能的迭代和简单需求的开发,可以简化流程,快速迭代。盖茅坑、猪圈、三居室还是不同的,不能一概而论
    • 做好技术分析、复盘、总结、归纳,沉淀出的技术资料非常有价值,既可以把项目开发经验传承给新人,也可以让所有人做好各自的技术成长。并且通过复盘和总结,又可以提炼出更多新的思路和提升技术氛围。
    • 好了,本章就总结到这,可能对具体的你或者具体的公司,会有不同的视角和结果。如果有一些好的点可以互相讨论学习。另外最近学会了个新东西分享给大家:内卷的反义词是:外包,合同的反义词是:离异!

    七、系列推荐


    博客:https://bugstack.cn
    Github:https://github.com/fuzhengwei/CodeGuide/wiki

    查看原文

    赞 31 收藏 19 评论 1

    木子雷 回答了问题 · 1月5日

    mysql在处理较大limit分页时候回表的疑问

    1、为什么需要回表:

    通过了解sql语句的执行顺序可以知道为什么需要回表了,limit限制条数是在select投影列后才执行的,所以需要先执行select投影列,由于投影列字段在c1索引树上不能获取到全部,那就需要回表了;

    查询sql语句的执行顺序可参考:

    FROM
    <表名> # 选取表,将多个表数据通过笛卡尔积变成一个表。
    ON
    <筛选条件> # 对笛卡尔积的虚表进行筛选
    JOIN <join, left join, right join...> 
    <join表> # 指定join,用于添加数据到on之后的虚表中,例如left join会将左表的剩余数据添加到虚表中
    WHERE
    <where条件> # 对上述虚表进行筛选
    GROUP BY
    <分组条件> # 分组
    <SUM()等聚合函数> # 用于having子句进行判断,在书写上这类聚合函数是写在having判断里面的
    HAVING
    <分组筛选> # 对分组后的结果进行聚合筛选
    SELECT
    <返回数据列表> # 返回的单列必须在group by子句中,聚合函数除外
    DISTINCT
    <数据除重>
    ORDER BY
    <排序条件> # 排序
    LIMIT
    <行数限制>

    2、解决方式:

    可以将sql语句改成下面这样,就会避免大量回表了:
    select b.* from (select id from test1 where c1>202 limit 10000,10)a, test1 b where a.id=b.id

    首先在c1普通索引树上获取到limit限制的 id主键值,然后再根据主键值去聚簇索引树上查询出需要的数据即可;常见的分页sql语句一般也是这样写,往后翻页时速度跟翻第一页一样快;

    关注 3 回答 1

    木子雷 发布了文章 · 1月4日

    一次对group by时间导致的慢查询的优化

    前言:

    最近在测试环境中点击一个图表展示页面时,半天才得到后台响应的数据进行页面渲染展示,后台的响应很慢,这样极大的降低了用户的体验;

    发现这个问题后马上进行了排查 ,通过排查发现是由一个查询很慢的 group by 语句导致的;

    本文主线:

    ①、简单描述下排查步骤;

    ②、对 group by 查询慢进行优化;

    简单描述下排查步骤:

    排查主要分为了两个步骤:

    • 后台接口的监控,看看哪个方法调用时耗时多
    • 数据库开启慢查询日志,记录执行很慢的SQL

    推荐使用阿里开源的Java线上诊断工具 Arthas ,使用其 trace 命令统计方法调用链路上各个方法节点的耗时;

    Arthas 工具的具体使用方法可参考: 线上服务响应时间太长的排查心路

    通过使用Arthas工具统计到一个进行数据库的 group by查询 方法耗时很严重;

    为了进一步确定是这个查询SQL 很耗时,将MySql 的慢查询日志开启了,然后再次调用后台这个接口,发现慢查询日志中确实存在了这个SQL语句;

    SQL语句如下:

    SELECT
        date_format(createts, '%Y') AS YEAR
    FROM
        t_test_log
    GROUP BY
        date_format(createts, '%Y')
    ORDER BY
        createts DESC
    这个SQL语句是用来统计表中所有数据被创建时的年份;

    下面就来聊聊这个SQL为什么会比较慢,然后进行了怎样的优化;

    对 group by 查询慢进行优化:

    在优化group by查询的时候,一般会想到下面这两个名词,通过下面这两种索引扫描可以高效快速的完成group by操作:

    • 松散索引扫描(Loose Index Scan)
    • 紧凑索引扫描(Tight Index Scan)

    group by操作在没有合适的索引可用时,通常先扫描整个表提取数据并创建一个临时表,然后按照group by指定的列进行排序;在这个临时表里面,对于每一个group 分组的数据行来说是连续在一起的。

    完成排序之后,就可以得到所有的groups 分组,并可以执行聚集函数(aggregate function)。

    可以看到,在没有使用索引的时候,需要创建临时表和排序;那在执行计划的 Extra 额外信息中通常就会看到这些信息 Using temporary; Using filesort 出现 。

    1、首先查看下SQL的执行计划:

    得到这个慢查询的SQL后,马上使用 explain 关键字分析其执行计划:

    通过查看执行计划发现,这个SQL语句走的是 全表扫描 ,并且通过扫描了大概 99974 行记录后才得到最终的结果集,并且执行过程中使用到了临时表和文件辅助排序;

    2、SQL执行计划内容简述:

    查看执行计划时,主要看上图中花圈的那三项数据即可:

    • type:访问类型,这是sql查询优化中一个很重要的指标,结果值从好到坏依次是:

    • Rows:数据行,根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数;
    • Extra:额外信息,SQL执行时十分重要的额外信息,简单说几个常会出现的值:

      • Using filesort : 未利用到索引的默认排序,需要使用文件辅助进行排序,出现其说明SQL性能不好;
      • Using temporary:使用临时表保存中间结果,常见于 group by ,出现其说明SQL性能不好;
      • Using index: 说明可以直接在索引树上就能得到最终的值,避免了回表,出现其说明SQL性能很好;
      • Using index for group-by:表示使用了 松散索引扫描 ,出现其说明SQL性能很好;因为松散索引扫描只需要读取很少量的数据就可以完成group by操作,所以执行效率非常高;
      • select tables optimized away: 在没有group by子句的情况下,基于索引优化 MIN/MAX 聚合函数操作,不必等到执行阶段在进行计算,查询执行计划生成的阶段即可完成优化,出现其说明SQL性能达到最优,往往配合 type访问类型的system 出现;

    3、建立索引后再查看执行计划:

    上面通过查看执行计划得知,因为没有创建相应的索引,所以走的是全表扫描,性能最差;然后对 createts 字段创建索引;再查看其执行计划:

    通过查看创建索引后的执行计划发现,此次查询走的 索引全扫描 ,此次虽然从全表扫描优化到了索引全扫描,但是还是需要通过扫描了大概 99974 行记录后才得到最终的结果集,性能并没有提升太多;

    并且发现 Extra 信息中还是存在 Using temporary; Using filesort ,说明没有使用到 松散索引扫描或紧凑索引扫描

    然后再次分析下SQL语句:

    SELECT
        date_format(createts, '%Y') AS YEAR
    FROM
        t_test_log
    GROUP BY
        date_format(createts, '%Y')
    ORDER BY
        createts DESC

    发现SQL中对索引字段 createts 做了 date_format 函数运算,所以才导致没使用上松散索引扫描或紧凑索引扫描;然后需要重写下SQL 。

    4、通过改写SQL进行优化:

    改写后的SQL如下:

    SELECT
        date_format(createts, '%Y') AS years
    FROM
        (
            SELECT
                createts
            FROM
                t_test_log
            GROUP BY
                createts
        ) t_test_log_1
    GROUP BY
        date_format(createts, '%Y')
    ORDER BY
        createts DESC

    改写完SQL后重新执行,发现查询速度快了非常多,性能上有了质的飞跃;

    然后又查看了下它的执行计划如下:

    查看上面那个嵌套查询SQL语句的执行计划,子查询部分的通过扫描大概52行记录就能得到结果集,相比于一开始需要扫描 99974 行 记录才能得到结果集,这个性能快了太多了;并且子查询的 Extra 信息中出现了 Using index for group-by ,说明使用到了松散索引扫描,效率才提升了这么多;

    外查询对子查询(52行记录)的结果集再次进行分组排序,此时采用的是全表(全结果集)的查询, 如果结果集很大的话,效率不会很高

    所以,在使用此优化方案的SQL语句时,需要统计下子查询的结果集的大小,如果子查询结果集很大的话,就不建议使用此方案了,可以尝试使用下面的这种优化方案;

    5、通过 改写SQL + 改写代码 进行优化:

    上面优化方案,只需改写SQL即可,无需对代码进行修改;本优化方案既要改写SQL,还要进行代码的修改;

    改写后的SQL如下: 这个SQL是查询出表中最小年份和最大年份

    (
        SELECT
            date_format(createts, '%Y') AS years
        FROM
            t_test_log
        ORDER BY
            createts
        LIMIT 1
    )
    UNION ALL
        (
            SELECT
                date_format(createts, '%Y') AS years
            FROM
                t_test_log
            ORDER BY
                createts DESC
            LIMIT 1
        )

    查看下上面这个SQL语句的执行计划:

    上面这个SQL是利用索引的默认排序,直接获取排序后的第一条记录,只需要扫描一行记录(rows :1)就能获取到最终的结果集;所以此SQL的性能是非常好的 。

    但是需要记住,这个SQL查询出的结果集不是最终需要的数据,需要 写代码 计算出最终的结果集:

    • 得到的最大最小年份这两个值 一样:说明表中的数据都是属于一个年份的
    • 得到的最大最小年份这两个值不一样:

      • 两个值相减得一:说明年份是挨着的两个年份,可以直接将结果集返回;
      • 两个值相减大于一:说明最小年份和最大年份之间还存在年份,通过计算得出中间年份

    但是注意,通过写代码计算出最终的年份,这种方式还是存在一个问题的,那就是确实表中根本没有中间年份的数据,但是通过计算却得出了;

    举例说明:假如通过SQL查询出了最小年份和最大年份是2018和2021,那么再通过代码计算出中间年份2019和2020,但是表中数据根本就不存在2019年份的数据,这是就会出现问题了;

    所以这种方案也需要根据自己具体的业务场景和实际的数据情况等分析是否需要采用 。

    扩展:

    在通过 改写SQL + 改写代码 进行优化时,改写的SQL不止上面那一种,还有一种查询效率也比较高的改写SQL;

    就是使用 min、max 聚合函数进行改写SQL,但是在使用聚合函数时,可以写出下面两种样式的SQL,到底哪种改写SQL效率是比较高呢,留个悬念,大家可以自行去分析尝试下哟! 可以在评论区留下你的答案呀!

    第一种改写SQL方式:

    (
        SELECT
            min(date_format(createts, '%Y')) AS years
        FROM
            t_test_log
    )
    UNION ALL
      (
            SELECT
                max(date_format(createts, '%Y')) AS years
            FROM
                t_test_log
       )

    第二种改写SQL方式:

    (
        SELECT
            date_format(minyear, '%Y') AS years
        FROM
            (
                SELECT
                    min(createts) AS minyear
                FROM
                    t_test_log
            ) t_test_log_1
    )
    UNION ALL
       (
            SELECT
                date_format(maxyear, '%Y') AS years
            FROM
                (
                    SELECT
                        max(createts) AS maxyear
                    FROM
                        t_test_log
                ) t_test_log_2
       )

    ❤ 点赞 + 评论 + 转发 哟

    如果本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创作的动力,谢谢啦!

    您可以微信搜索【木子雷】公众号,大量Java学习干货文章,您可以来瞧一瞧哟!

    查看原文

    赞 0 收藏 0 评论 0

    木子雷 赞了文章 · 1月3日

    【Mysql】MySQL参数:innodb_flush_log_at_trx_commit 和 sync_binlog

    innodb_flush_log_at_trx_commit 和 sync_binlog 是 MySQL 的两个配置参数。它们的配置对于 MySQL 的性能有很大影响(一般为了保证数据的不丢失,会设置为双1,该情形下数据库的性能也是最低的)。

    1、innodb_flush_log_at_trx_commit

    innodb_flush_log_at_trx_commit:是 InnoDB 引擎特有的,ib_logfile的刷新方式( ib_logfile:记录的是redo log和undo log的信息)

    取值:0/1/2

    innodb_flush_log_at_trx_commit=0,表示每隔一秒把log buffer刷到文件系统中(os buffer)去,并且调用文件系统的“flush”操作将缓存刷新到磁盘上去。也就是说一秒之前的日志都保存在日志缓冲区,也就是内存上,如果机器宕掉,可能丢失1秒的事务数据。

    innodb_flush_log_at_trx_commit=1,表示在每次事务提交的时候,都把log buffer刷到文件系统中(os buffer)去,并且调用文件系统的“flush”操作将缓存刷新到磁盘上去。这样的话,数据库对IO的要求就非常高了,如果底层的硬件提供的IOPS比较差,那么MySQL数据库的并发很快就会由于硬件IO的问题而无法提升。

    innodb_flush_log_at_trx_commit=2,表示在每次事务提交的时候会把log buffer刷到文件系统中去,但并不会立即刷写到磁盘。如果只是MySQL数据库挂掉了,由于文件系统没有问题,那么对应的事务数据并没有丢失。只有在数据库所在的主机操作系统损坏或者突然掉电的情况下,数据库的事务数据可能丢失1秒之类的事务数据。这样的好处,减少了事务数据丢失的概率,而对底层硬件的IO要求也没有那么高(log buffer写到文件系统中,一般只是从log buffer的内存转移的文件系统的内存缓存中,对底层IO没有压力)。

    2、sync_binlog

    sync_binlog:是MySQL 的二进制日志(binary log)同步到磁盘的频率。

    取值:0-N

    sync_binlog=0,当事务提交之后,MySQL不做fsync之类的磁盘同步指令刷新binlog_cache中的信息到磁盘,而让Filesystem自行决定什么时候来做同步,或者cache满了之后才同步到磁盘。这个是性能最好的。

    sync_binlog=1,当每进行1次事务提交之后,MySQL将进行一次fsync之类的磁盘同步指令来将binlog_cache中的数据强制写入磁盘。

    sync_binlog=n,当每进行n次事务提交之后,MySQL将进行一次fsync之类的磁盘同步指令来将binlog_cache中的数据强制写入磁盘。

    注:

    大多数情况下,对数据的一致性并没有很严格的要求,所以并不会把 sync_binlog 配置成 1. 为了追求高并发,提升性能,可以设置为 100 或直接用 0.
    而和 innodb_flush_log_at_trx_commit 一样,对于支付服务这样的应用,还是比较推荐 sync_binlog = 1.

    查看原文

    赞 3 收藏 2 评论 0

    认证与成就

    • 获得 23 次点赞
    • 获得 6 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 3 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2020-03-19
    个人主页被 2.7k 人浏览