IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 第14章 Spring的MySQLMaxValueIncrementer深入分析与扩展 -> 正文阅读

[Java知识库]第14章 Spring的MySQLMaxValueIncrementer深入分析与扩展

取号这种需求在开发中有时候很常见,例如:银行办理业务时候就需求取号。那如何去实现一种高效、可靠的取号呢?对于取号这种需求,我们首先需要保证取号不能重复,也就是说保证分布式系统取号也不能重复。有可能我们会首先想到基于Redis的incr命令可以高效、快速实现,但是缺点是当Redis宕机后,有可能造成数据丢失,导致取号重复。另一种方案就是基于MySQL实现,也就是今天的主题 MySQLMaxValueIncrementer

源码分析

创建 MySQLMaxValueIncrementer 对象时,需要指定表名 incrementerName 和 需要递增的列名 columnName 。每次调用 nextIntValue() 就对列递增1。该功能就基于MySQL的 last_insert_id() 函数来实现。为什么 last_insert_id() 函数就能保证并发问题呢?因为 last_insert_id()Connection 对象有关,也就是相当于每个 connection 对象都自己的 last_insert_id() 函数,并且 connection 对象是访问不到其他 connection 对象的 lsat_insert_id() 函数。这样就巧妙保证了并发问题。核心代码如下所示:

protected synchronized long getNextKey() throws DataAccessException {
        if (this.maxId == this.nextId) {
            Connection con = null;
            Statement stmt = null;
            boolean mustRestoreAutoCommit = false;

            try {
                // 如果为true表示每次都使用新创建的连接
                if (this.useNewConnection) {
                    con = this.getDataSource().getConnection();
                    if (con.getAutoCommit()) {
                        mustRestoreAutoCommit = true;
                        con.setAutoCommit(false);
                    }
                } else {
                    // 否则使用连接池里的连接
                    con = DataSourceUtils.getConnection(this.getDataSource());
                }

                stmt = con.createStatement();
                if (!this.useNewConnection) {
                    DataSourceUtils.applyTransactionTimeout(stmt, this.getDataSource());
                }

                String columnName = this.getColumnName();

                try {
                    stmt.executeUpdate("update " + this.getIncrementerName() + " set " + columnName + " = last_insert_id(" + columnName + " + " + this.getCacheSize() + ") limit 1");
                } catch (SQLException var20) {
                    throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + this.getIncrementerName() + " sequence table", var20);
                }

                ResultSet rs = stmt.executeQuery("select last_insert_id()");

                try {
                    if (!rs.next()) {
                        throw new DataAccessResourceFailureException("last_insert_id() failed after executing an update");
                    }

                    this.maxId = rs.getLong(1);
                } finally {
                    JdbcUtils.closeResultSet(rs);
                }

                this.nextId = this.maxId - (long)this.getCacheSize() + 1L;
            } catch (SQLException var23) {
                throw new DataAccessResourceFailureException("Could not obtain last_insert_id()", var23);
            } finally {
                JdbcUtils.closeStatement(stmt);
                if (con != null) {
                    if (this.useNewConnection) {
                        try {
                            con.commit();
                            if (mustRestoreAutoCommit) {
                                con.setAutoCommit(true);
                            }
                        } catch (SQLException var21) {
                            throw new DataAccessResourceFailureException("Unable to commit new sequence value changes for " + this.getIncrementerName());
                        }

                        JdbcUtils.closeConnection(con);
                    } else {
                        DataSourceUtils.releaseConnection(con, this.getDataSource());
                    }
                }

            }
        } else {
            ++this.nextId;
        }

        return this.nextId;
    }

扩展

从上面的源码我们了解到,要使用 MySQLMaxValueIncrementer ,我们就必须有一张表,并且需要预先插入一条数据,才能使用该功能。有时候我们的取号在很多地方都需要使用,按照原来的方式要在每张表里都得有一个字段来记录当前序号值。这样维护可能变得有点困难。这里扩展就是把取号单独抽取一张表里,由业务KEY来关联当前序号,这样会相比前一种方式变得更加好维护。

类图设计

取号的实现不一定要用MySQL来实现,也可以基于Redis来实现,所以实现的方式有多种方式,考虑到可扩展,采用策略者设计模式来设计。

接口设计
public interface SequenceGenerator {

    int nextIntValue(Sequence sequence);

    long nextLongValue(Sequence sequence);
}
public interface Sequence {

    String getKey();
}
实现类

StableSequenceGenerator实现了 SequenceGeneratorInitializingBean。该类会在初始化时去创建一张 Sequence 表,用来维护各个业务的序号。使用 insert into ... on duplicate key 的方式,不需要手动插入初始化数据。

public class StableSequenceGenerator implements SequenceGenerator, InitializingBean {

    private final DataSource dataSource;

    private static final String TABLE_NAME = "sequence";

    private static final String COLUMN_NAME = "current_seq";

    private static final String TABLE_SCHEME = "create table sequence(" +
            "current_seq int not null default '1' comment '当前序号', " +
            "`type` varchar(255) not null comment '业务类型'," +
            "`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
            "`updated_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
            "unique key `uniq_type` (`type`)" +
            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;";

    public StableSequenceGenerator(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.initTable();
    }

    private void initTable() {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            conn = this.dataSource.getConnection();
            DatabaseMetaData metaData = conn.getMetaData();
            rs = metaData.getTables(conn.getCatalog(), null, TABLE_NAME, null);
            stmt = conn.createStatement();
            if (!rs.next()) {
                stmt.execute(TABLE_SCHEME);
            }
        } catch (SQLException e) {
            throw new DataAccessResourceFailureException("Unable to create table " + TABLE_NAME);
        } finally {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(stmt);
            JdbcUtils.closeConnection(conn);
        }
    }

    @Override
    public int nextIntValue(Sequence sequence) {
        ExtendMySQLMaxValueIncrementer incrementer = new ExtendMySQLMaxValueIncrementer(dataSource, sequence);
        incrementer.setIncrementerName(TABLE_NAME);
        incrementer.setColumnName(COLUMN_NAME);
        return incrementer.nextIntValue();
    }

    @Override
    public long nextLongValue(Sequence sequence) {
        ExtendMySQLMaxValueIncrementer incrementer = new ExtendMySQLMaxValueIncrementer(dataSource, sequence);
        incrementer.setIncrementerName(TABLE_NAME);
        incrementer.setColumnName(COLUMN_NAME);
        return incrementer.nextLongValue();
    }
}
MySQLMaxValueIncrementer的扩展实现
public class ExtendMySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer {

    private static final String VALUE_SQL = "select last_insert_id()";
    private long nextId = 0L;
    private long maxId = 0L;
    private boolean useNewConnection = true;

    private Sequence sequence;

    public ExtendMySQLMaxValueIncrementer() {
    }

    public ExtendMySQLMaxValueIncrementer(DataSource dataSource, Sequence sequence) {
        super(dataSource, "sequence", "current_seq");
        this.sequence = sequence;
    }

    public ExtendMySQLMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) {
        super(dataSource, incrementerName, columnName);
    }

    public void setUseNewConnection(boolean useNewConnection) {
        this.useNewConnection = useNewConnection;
    }

    @Override
    protected synchronized long getNextKey() throws DataAccessException {
        if (this.maxId == this.nextId) {
            Connection con = null;
            Statement stmt = null;
            boolean mustRestoreAutoCommit = false;

            try {
                if (this.useNewConnection) {
                    con = this.getDataSource().getConnection();
                    if (con.getAutoCommit()) {
                        mustRestoreAutoCommit = true;
                        con.setAutoCommit(false);
                    }
                } else {
                    con = DataSourceUtils.getConnection(this.getDataSource());
                }

                stmt = con.createStatement();
                if (!this.useNewConnection) {
                    DataSourceUtils.applyTransactionTimeout(stmt, this.getDataSource());
                }

                String columnName = this.getColumnName();

                try {
                    stmt.executeUpdate(this.getSql());
                } catch (SQLException var20) {
                    throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + this.getIncrementerName() + " sequence table", var20);
                }

                ResultSet rs = stmt.executeQuery("select last_insert_id()");

                try {
                    if (!rs.next()) {
                        throw new DataAccessResourceFailureException("last_insert_id() failed after executing an update");
                    }

                    this.maxId = rs.getLong(1);
                    // 如果第一次是insert时,last_insert_id() 返回的是0
                    this.maxId = this.maxId == 0L ? 1 : this.maxId;
                } finally {
                    JdbcUtils.closeResultSet(rs);
                }

                this.nextId = this.maxId - (long)this.getCacheSize() + 1L;
            } catch (SQLException var23) {
                throw new DataAccessResourceFailureException("Could not obtain last_insert_id()", var23);
            } finally {
                JdbcUtils.closeStatement(stmt);
                if (con != null) {
                    if (this.useNewConnection) {
                        try {
                            con.commit();
                            if (mustRestoreAutoCommit) {
                                con.setAutoCommit(true);
                            }
                        } catch (SQLException var21) {
                            throw new DataAccessResourceFailureException("Unable to commit new sequence value changes for " + this.getIncrementerName());
                        }

                        JdbcUtils.closeConnection(con);
                    } else {
                        DataSourceUtils.releaseConnection(con, this.getDataSource());
                    }
                }

            }
        } else {
            ++this.nextId;
        }

        return this.nextId;
    }

    public String getSql() {
        return "insert into " + this.getIncrementerName() + " (type)" + " values('" + this.sequence.getKey() +"')" +
                " ON DUPLICATE KEY UPDATE " + this.getColumnName() + " = last_insert_id(" + this.getColumnName() + " + " + this.getCacheSize() + ")";
    }
}
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-04-24 09:15:04  更:2022-04-24 09:19:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 2:56:38-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码