├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── hyd │ │ └── mysqlsequencegenerator │ │ ├── MysqlSequenceGenerator.java │ │ └── MysqlSequenceGeneratorApplication.java └── resources │ └── application.properties └── test └── java └── com └── hyd └── mysqlsequencegenerator ├── MyCompanyTest.java └── MysqlSequenceGeneratorTest.java /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*.{java,xml,properties}] 3 | indent_style = space 4 | indent_size = 4 5 | charset = utf-8 6 | end_of_line = lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | 11 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 12 | !/.mvn/wrapper/maven-wrapper.jar 13 | 14 | .idea/ 15 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysql-sequence-generator 2 | 3 | 基于 MySQL 的 `last_insert_id()` 函数而写的序列生成工具。 4 | 5 | 效率很高,没有数据库事务。 6 | 7 | **整个项目只有一个类,且无任何依赖关系,可以直接拷贝到任何项目中使用。** 8 | 9 | [源码 MysqlSequenceGenerator.java](https://github.com/yiding-he/mysql-sequence-generator/blob/master/src/main/java/com/hyd/mysqlsequencegenerator/MysqlSequenceGenerator.java) 10 | 11 | ### 标准序列表 12 | 13 | 下面是标准的序列表,但也可兼容字段稍微不同的表,具体见后面说明。 14 | 15 | ```sql 16 | create table t_sequence( 17 | name varchar(100) primary key comment '序列名称', 18 | code varchar(5) not null default '00000' comment '序列编号(用于区分不同序列种类)', 19 | value bigint not null default 0 comment '当前值', 20 | min bigint not null default 0 comment '最小值', 21 | max bigint not null default 99999999 comment '最大值,当 value 达到最大值时重新从 min 开始', 22 | step int not null default 1000 comment '步长,每次取序列时缓存多少,步长越大,数据库访问频率越低' 23 | ); 24 | 25 | INSERT into t_sequence (name, code, max) values ('seq1', '886', 99999999); 26 | INSERT into t_sequence (name, code, max) values ('seq2', '887', 99999999); 27 | INSERT into t_sequence (name, code, max) values ('seq3', '888', 99999999); 28 | ``` 29 | 30 | 31 | ### 一、构造方法(含兼容性配置) 32 | 33 | MysqlSequenceGenerator 不要求表一定要是标准的样子,但要求表中至少要有下面五个字段: 34 | 35 | - 序列名称 36 | - 当前值 37 | - 最小值 38 | - 最大值 39 | - 步长 40 | 41 | 每个字段可以自定义名字,而且标准表中的 `code` 字段是可选的。下面是一个创建 `MysqlSequenceGenerator` 对象的例子: 42 | 43 | ```java 44 | // 1. 使用标准的序列表来创建 MysqlSequenceGenerator 45 | MysqlSequenceGenerator idGenerator = new MysqlSequenceGenerator(dataSource); 46 | 47 | // 2. 使用非标准的序列表来创建 MysqlSequenceGenerator 48 | MysqlSequenceGenerator idGenerator = new MysqlSequenceGenerator( 49 | dataSource::getConnection, // 获得 Connection 的方式 50 | Connection::close, // 关闭 Connection 的方式 51 | "t_sequence", // 实际的序列表名称 52 | false, // 是否异步获取新的序列号。提前异步获取新的序列号可提升稍许性能 53 | Arrays.asList( // 自定义字段名 54 | ColumnInfo.customName(Column.Min, "min_value"), // 最小值字段的实际字段名为 min_value 55 | ColumnInfo.customName(Column.Max, "max_value"), // 最大值字段的实际字段名为 max_value 56 | ColumnInfo.undefined(Column.Code) // 表中没有 code 字段 57 | ) 58 | ); 59 | ``` 60 | 61 | ### 二、使用方法 62 | 63 | 1. 获得一个 long 类型的序列:`long id = idGenerator.nextLong("seq1")` 64 | 1. 获得一个带年月日和编号的字符串序列:`String id = idGenerator.nextSequence("seq1")` 65 | 1. 获得一个带年月日和自定义编号的字符串序列:`String id = idGenerator.nextSequence("seq1", "001")` 66 | 67 | 使用的示例详见单元测试。 68 | 69 | #### 字符串序列的格式 70 | 71 | 字符串序列的格式为 `yyyyMMdd[code][sequence]`,其中: 72 | 73 | - `[code]` 为 code 字段的值(如果有的话); 74 | - `[sequence]` 的长度与 max 字段的值长度相同。 75 | 76 | 例如某个序列,code 字段值为 `888`,max 字段值为 `999999`,那么生成的字符串序列可能为 `"20191231888000001"`。 77 | 78 | ### 三、兼容 Spring 数据库事务 79 | 80 | 如果要在 Spring Boot 项目中使用并兼容 Spring 事务,请参考 81 | `com.hyd.mysqlsequencegenerator.MysqlSequenceGeneratorApplication` [源码示例](https://github.com/yiding-he/mysql-sequence-generator/blob/master/src/main/java/com/hyd/mysqlsequencegenerator/MysqlSequenceGeneratorApplication.java)。 82 | 83 | ### 四、原理简介 84 | 85 | 1. MySQL 支持在 update 语句中使用 `last_insert_id(xxx)` 来临时保存最近生成的 ID 到会话中,接下来可以在同一会话中用 86 | `select last_insert_id()` 来获取刚生成的 ID。 87 | 1. `MysqlSequenceGenerator` 利用 `AtomicLong` 的 `updateAndGet()` 方法来完成计数器更新和数据库操作。 88 | 1. `Counter` 是实现并发序列的核心类,它包含一个用于保存自增值的 `AtomicLong` 对象和一个 `Threshold` 89 | 阈值对象,后者表示前者自增到什么值时需要从数据库重新取一个段。取到后,根据取到的最大值将 Threshold 的阈值推高。 90 | 1. 当多个线程同时从数据库重新取段时,会分别将 Threshold 的阈值推高数次。 91 | 92 | ### 五、性能 93 | 94 | 因为相关的 SQL 都非常简单,性能瓶颈其实都在数据库上。步长越小,数据库操作越频繁。 95 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.hyd 8 | mysql-sequence-generator 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 1.8 13 | 1.8 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-dependencies 22 | 2.0.4.RELEASE 23 | pom 24 | import 25 | 26 | 27 | 28 | 29 | 30 | 31 | org.apache.commons 32 | commons-dbcp2 33 | 2.7.0 34 | provided 35 | 36 | 37 | mysql 38 | mysql-connector-java 39 | 8.0.29 40 | provided 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-test 45 | test 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter 50 | provided 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-jdbc 55 | provided 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-configuration-processor 60 | provided 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/com/hyd/mysqlsequencegenerator/MysqlSequenceGenerator.java: -------------------------------------------------------------------------------- 1 | package com.hyd.mysqlsequencegenerator; 2 | 3 | import javax.sql.DataSource; 4 | import java.sql.Connection; 5 | import java.sql.PreparedStatement; 6 | import java.sql.ResultSet; 7 | import java.sql.SQLException; 8 | import java.time.LocalDate; 9 | import java.time.format.DateTimeFormatter; 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.concurrent.*; 15 | import java.util.concurrent.atomic.AtomicLong; 16 | import java.util.function.BiConsumer; 17 | import java.util.function.Supplier; 18 | 19 | import static java.util.Optional.ofNullable; 20 | 21 | /** 22 | * https://github.com/yiding-he/mysql-sequence-generator 23 | */ 24 | public class MysqlSequenceGenerator { 25 | 26 | public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); 27 | 28 | public static final String DEFAULT_UPDATE = "update #table# " + 29 | "set #seqvalue# = last_insert_id(" + 30 | " if(#seqvalue# + #step# > #max#, #min#, #seqvalue# + #step#)" + 31 | ") where #seqname# = ?"; 32 | 33 | public static final String SEQ_SEGMENT_QUERY = "SELECT " + 34 | "last_insert_id(), last_insert_id() + #step# FROM #table# " + 35 | "where #seqname# = ?"; 36 | 37 | private static final String CODE_COLUMN = "#code# code,"; 38 | 39 | public static final String SEQ_CODE_QUERY = "select {{CODE_COLUMN}} #max# " 40 | + "from #table# where #seqname# = ?"; 41 | 42 | public static final String DEFAULT_TABLE_NAME = "t_sequence"; 43 | 44 | public static final int DEFAULT_INFO_CACHE_EXPIRE_MILLIS = 60000; 45 | 46 | //////////////////////////////////////////////////////////// 47 | 48 | @FunctionalInterface 49 | interface F { 50 | 51 | B f(A a) throws SQLException; 52 | } 53 | 54 | @FunctionalInterface 55 | interface ConnectionSupplier { 56 | 57 | Connection get() throws SQLException; 58 | } 59 | 60 | @FunctionalInterface 61 | interface ConnectionCloser { 62 | 63 | void close(Connection connection) throws SQLException; 64 | } 65 | 66 | static class TimedCache { 67 | 68 | class CacheItem { 69 | 70 | private final long expiry; 71 | 72 | private final T value; 73 | 74 | public CacheItem(T value) { 75 | this.expiry = System.currentTimeMillis() + maxAgeMillis; 76 | this.value = value; 77 | } 78 | 79 | public boolean expired() { 80 | return System.currentTimeMillis() > expiry; 81 | } 82 | } 83 | 84 | private final int maxAgeMillis; 85 | 86 | private final ConcurrentHashMap> cache = new ConcurrentHashMap<>(); 87 | 88 | public TimedCache() { 89 | this(DEFAULT_INFO_CACHE_EXPIRE_MILLIS); 90 | } 91 | 92 | public TimedCache(int maxAgeMillis) { 93 | this.maxAgeMillis = maxAgeMillis; 94 | } 95 | 96 | public T get(String key, Supplier supplier) { 97 | CacheItem item = cache.get(key); 98 | if (item == null || item.expired()) { 99 | synchronized (cache) { 100 | item = cache.get(key); 101 | if (item == null || item.expired()) { 102 | T value = supplier.get(); 103 | if (value != null) { 104 | item = new CacheItem<>(value); 105 | cache.put(key, item); 106 | } 107 | } 108 | } 109 | } 110 | return item == null ? null : item.value; 111 | } 112 | 113 | public void clear() { 114 | cache.clear(); 115 | } 116 | } 117 | 118 | static class SeqInfo { 119 | 120 | private final String code; 121 | 122 | private final long max; 123 | 124 | public SeqInfo(String code, long max) { 125 | this.code = code; 126 | this.max = max; 127 | } 128 | } 129 | 130 | static class Threshold { 131 | 132 | final long max; // max value for current section 133 | 134 | final long threshold; // threshold value for when to fetch new section 135 | 136 | Threshold(long max, long threshold) { 137 | this.max = max; 138 | this.threshold = threshold; 139 | } 140 | 141 | boolean needFetch(long seq) { 142 | return seq + 1 >= threshold; 143 | } 144 | 145 | boolean runOut(long seq) { 146 | return seq + 1 >= max; 147 | } 148 | } 149 | 150 | /** 151 | * Customizable columns 152 | */ 153 | public enum Column { 154 | Name("name"), 155 | Code("code"), 156 | Value("value"), 157 | Min("min"), 158 | Max("max"), 159 | Step("step"); 160 | 161 | private final String defaultColumnName; 162 | 163 | Column(String defaultColumnName) { 164 | this.defaultColumnName = defaultColumnName; 165 | } 166 | 167 | public String getDefaultColumnName() { 168 | return defaultColumnName; 169 | } 170 | } 171 | 172 | /** 173 | * Column info after customization 174 | */ 175 | public static class ColumnInfo { 176 | 177 | private final Column column; 178 | 179 | private final String value; 180 | 181 | private ColumnInfo(Column column, String value) { 182 | this.column = column; 183 | this.value = value; 184 | } 185 | 186 | public static ColumnInfo undefined(Column c) { 187 | return new ColumnInfo(c, null); 188 | } 189 | 190 | public static ColumnInfo defaultName(Column c) { 191 | return new ColumnInfo(c, c.getDefaultColumnName()); 192 | } 193 | 194 | public static ColumnInfo customName(Column c, String name) { 195 | return new ColumnInfo(c, name); 196 | } 197 | } 198 | 199 | public static class MysqlSequenceException extends RuntimeException { 200 | 201 | public MysqlSequenceException(Throwable cause) { 202 | super(cause); 203 | } 204 | 205 | public MysqlSequenceException(String message) { 206 | super(message); 207 | } 208 | } 209 | 210 | ////////////////////////////////////////////////////////////// 211 | 212 | /** 213 | * 每个序列对应一个 Counter 对象 214 | * 注意:对会被多线程访问的成员,要么是 final 的,要么就必须是 volatile 的。 215 | */ 216 | class Counter { 217 | 218 | final String sequenceName; // 序列名称 219 | 220 | final AtomicLong value = new AtomicLong(); // 序列的当前值 221 | 222 | volatile Threshold threshold; // 当前队列的阈值,用来判断是否该从数据库取下一个序列段 223 | 224 | volatile Future future; // 如果是异步取下一序列段,则要用到 Future 225 | 226 | public Counter(String sequenceName, long min, long max) { 227 | this.sequenceName = sequenceName; 228 | this.value.set(min); 229 | this.threshold = new Threshold(max, max); 230 | } 231 | 232 | public long next() throws MysqlSequenceException { 233 | try { 234 | // updateAndGet() 方法能够保证操作的原子性 235 | return value.updateAndGet(seq -> asyncFetch ? nextAsync(seq) : nextSync(seq)); 236 | } catch (Exception e) { 237 | throw new MysqlSequenceException(e); 238 | } 239 | } 240 | 241 | /** 242 | * 取下一个序号,如果需要的话,以异步方式从数据库取新的序号段 243 | * 244 | * @param seq 当前序号 245 | */ 246 | private long nextAsync(long seq) { 247 | 248 | // 判断是否该取序号段了,以及是否正在取序号段 249 | if (threshold.needFetch(seq) && future == null) { 250 | synchronized (this) { 251 | // 进入这里但发现不满足,意味着:1)threshold 已经更新,或2)有其他线程已经开始取了 252 | if (threshold.needFetch(seq) && future == null) { 253 | future = asyncFetcher.submit(() -> { 254 | long[] minMax = updateSequence(); 255 | // 除以 2 的意思是当序号段用掉一半时,触发异步取下一个序号段 256 | long t = (minMax[1] - minMax[0]) / 2 + minMax[0]; 257 | threshold = new Threshold(minMax[1], t); 258 | return minMax[0]; 259 | }); 260 | } 261 | } 262 | } 263 | 264 | // 如果当前序号段仍然可用,则取当前序号段下一个值, 265 | // 否则从 future.get() 取值。注意: 266 | // 1、future.get() 会阻塞; 267 | // 2、future.get() 成功完成时,threshold 已经被更新 268 | if (threshold.runOut(seq)) { 269 | synchronized (this) { 270 | if (threshold.runOut(seq)) { 271 | try { 272 | final Long l = future.get(); 273 | future = null; 274 | return l; 275 | } catch (Exception e) { 276 | throw new RuntimeException(e); 277 | } 278 | } else { 279 | return seq + 1; 280 | } 281 | } 282 | } else { 283 | return seq + 1; 284 | } 285 | } 286 | 287 | /** 288 | * 取下一个序号,如果需要的话,以同步方式从数据库取新的序号段 289 | * 290 | * @param seq 当前序号 291 | */ 292 | private long nextSync(long seq) { 293 | if (threshold.needFetch(seq)) { 294 | synchronized (this) { 295 | if (threshold.needFetch(seq)) { 296 | long[] minMax = updateSequence(); 297 | threshold = new Threshold(minMax[1], minMax[1]); 298 | return minMax[0]; 299 | } else { 300 | return seq + 1; 301 | } 302 | } 303 | } else { 304 | return seq + 1; 305 | } 306 | } 307 | 308 | /** 309 | * 从数据库取下一个序列段,并返回该段的最小值和最大值 310 | */ 311 | private long[] updateSequence() { 312 | return withConnection(connection -> { 313 | executeUpdateTemplate(connection, sequenceName); 314 | return querySegment(connection, sequenceName); 315 | }); 316 | } 317 | } 318 | 319 | //////////////////////////////////////////////////////////// 320 | 321 | private final ConnectionSupplier connectionSupplier; 322 | 323 | private final ConnectionCloser connectionCloser; 324 | 325 | private final String tableName; 326 | 327 | private final String updateTemplate; 328 | 329 | private final String querySegmentTemplate; 330 | 331 | private final String queryCodeTemplate; 332 | 333 | private final TimedCache seqInfoCache; 334 | 335 | private final Map counters = new ConcurrentHashMap<>(); 336 | 337 | private final Map seqInfoMap = new ConcurrentHashMap<>(); 338 | 339 | private final Map columnInfoMap; // set by user 340 | 341 | private final boolean asyncFetch; 342 | 343 | private final ExecutorService asyncFetcher = new ThreadPoolExecutor( 344 | 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>() 345 | ); 346 | 347 | private BiConsumer onSequenceUpdate; 348 | 349 | ////////////////////////////////////////////////////////////// 350 | 351 | public static class Config { 352 | 353 | private String tableName = DEFAULT_TABLE_NAME; 354 | 355 | private int infoCacheExpireMillis = DEFAULT_INFO_CACHE_EXPIRE_MILLIS; 356 | 357 | private boolean asyncFetch; 358 | 359 | private List columnInfos = new ArrayList<>(); 360 | 361 | public int getInfoCacheExpireMillis() { 362 | return infoCacheExpireMillis; 363 | } 364 | 365 | public Config setInfoCacheExpireMillis(int infoCacheExpireMillis) { 366 | this.infoCacheExpireMillis = infoCacheExpireMillis; 367 | return this; 368 | } 369 | 370 | public String getTableName() { 371 | return tableName; 372 | } 373 | 374 | public Config setTableName(String tableName) { 375 | this.tableName = tableName; 376 | return this; 377 | } 378 | 379 | public boolean isAsyncFetch() { 380 | return asyncFetch; 381 | } 382 | 383 | public Config setAsyncFetch(boolean asyncFetch) { 384 | this.asyncFetch = asyncFetch; 385 | return this; 386 | } 387 | 388 | public List getColumnInfos() { 389 | return columnInfos; 390 | } 391 | 392 | public Config setColumnInfos(List columnInfos) { 393 | this.columnInfos = columnInfos; 394 | return this; 395 | } 396 | } 397 | 398 | /** 399 | * Constructor. 400 | * 401 | * @param dataSource DataSource object 402 | */ 403 | public MysqlSequenceGenerator(DataSource dataSource) { 404 | this(dataSource::getConnection, Connection::close, new Config()); 405 | } 406 | 407 | /** 408 | * Constructor. 409 | * 410 | * @param dataSource DataSource object 411 | */ 412 | public MysqlSequenceGenerator(DataSource dataSource, Config config) { 413 | this(dataSource::getConnection, Connection::close, config); 414 | } 415 | 416 | /** 417 | * Constructor. 418 | * 419 | * @param connectionSupplier how to get a JDBC Connection object before database operation 420 | */ 421 | public MysqlSequenceGenerator(ConnectionSupplier connectionSupplier) { 422 | this(connectionSupplier, Connection::close, new Config()); 423 | } 424 | 425 | /** 426 | * Constructor. 427 | * 428 | * @param connectionSupplier how to get a JDBC Connection object before database operation 429 | * @param config customizations 430 | */ 431 | public MysqlSequenceGenerator(ConnectionSupplier connectionSupplier, Config config) { 432 | this(connectionSupplier, Connection::close, config); 433 | } 434 | 435 | /** 436 | * Constructor. 437 | * 438 | * @param connectionSupplier how to get a JDBC Connection object before database operation 439 | * @param connectionCloser how to deal with Connection object after database operation 440 | * @param config customizations 441 | */ 442 | public MysqlSequenceGenerator( 443 | ConnectionSupplier connectionSupplier, ConnectionCloser connectionCloser, Config config 444 | ) { 445 | 446 | List columnInfos = config.columnInfos; 447 | String tableName = config.tableName; 448 | boolean asyncFetch = config.asyncFetch; 449 | int infoCacheExpireMillis = config.infoCacheExpireMillis; 450 | 451 | // Create and fill this.columnInfoMap 452 | Map cmap = new HashMap<>(); 453 | columnInfos.forEach(info -> cmap.put(info.column, info)); 454 | for (Column c : Column.values()) { 455 | if (!cmap.containsKey(c)) { 456 | cmap.put(c, ColumnInfo.defaultName(c)); 457 | } 458 | } 459 | this.columnInfoMap = cmap; 460 | 461 | // Prepare SQL templates 462 | tableName = ofNullable(tableName).orElse(DEFAULT_TABLE_NAME); 463 | String seqNameColumn = cmap.get(Column.Name).value; 464 | String seqCodeColumn = cmap.get(Column.Code).value; 465 | String seqValueColumn = cmap.get(Column.Value).value; 466 | String seqMinColumn = cmap.get(Column.Min).value; 467 | String seqMaxColumn = cmap.get(Column.Max).value; 468 | String seqStepColumn = cmap.get(Column.Step).value; 469 | 470 | this.connectionSupplier = connectionSupplier; 471 | this.connectionCloser = connectionCloser; 472 | 473 | this.updateTemplate = DEFAULT_UPDATE 474 | .replace("#table#", tableName) 475 | .replace("#seqvalue#", seqValueColumn) 476 | .replace("#step#", seqStepColumn) 477 | .replace("#seqname#", seqNameColumn) 478 | .replace("#min#", getColumnName(Column.Min) == null ? "0" : seqMinColumn) 479 | .replace("#max#", seqMaxColumn); 480 | 481 | this.querySegmentTemplate = SEQ_SEGMENT_QUERY 482 | .replace("#table#", tableName) 483 | .replace("#seqname#", seqNameColumn) 484 | .replace("#step#", seqStepColumn); 485 | 486 | this.queryCodeTemplate = SEQ_CODE_QUERY 487 | .replace("{{CODE_COLUMN}}", getColumnName(Column.Code) == null ? "" : CODE_COLUMN) 488 | .replace("#code#", getColumnName(Column.Code) == null ? "" : seqCodeColumn) 489 | .replace("#max#", seqMaxColumn) 490 | .replace("#table#", tableName) 491 | .replace("#seqname#", seqNameColumn); 492 | 493 | this.asyncFetch = asyncFetch; 494 | this.seqInfoCache = new TimedCache<>(infoCacheExpireMillis); 495 | this.tableName = tableName; 496 | } 497 | 498 | /** 499 | * Constructor. 500 | * 501 | * @param connectionSupplier how to get a JDBC Connection object before database operation 502 | * @param connectionCloser how to deal with Connection object after database operation 503 | * @param tableName (nullable) customized sequence table name 504 | * @param asyncFetch whether to fetch new segment asynchronously 505 | * @param columnInfos column customizations 506 | */ 507 | public MysqlSequenceGenerator( 508 | ConnectionSupplier connectionSupplier, ConnectionCloser connectionCloser, 509 | String tableName, boolean asyncFetch, List columnInfos 510 | ) { 511 | this(connectionSupplier, connectionCloser, 512 | new Config() 513 | .setTableName(tableName) 514 | .setAsyncFetch(asyncFetch) 515 | .setColumnInfos(columnInfos) 516 | ); 517 | } 518 | 519 | public String getUpdateTemplate() { 520 | return updateTemplate; 521 | } 522 | 523 | public String getQuerySegmentTemplate() { 524 | return querySegmentTemplate; 525 | } 526 | 527 | public String getQueryCodeTemplate() { 528 | return queryCodeTemplate; 529 | } 530 | 531 | public void setOnSequenceUpdate(BiConsumer onSequenceUpdate) { 532 | this.onSequenceUpdate = onSequenceUpdate; 533 | } 534 | 535 | //////////////////////////////////////////////////////////// query functions 536 | 537 | /** 538 | * Get next numeric value of sequence 539 | */ 540 | public Long nextLong(String sequenceName) throws MysqlSequenceException { 541 | Counter counter = counters.computeIfAbsent(sequenceName, s -> new Counter(s, 0, 0)); 542 | return counter.next(); 543 | } 544 | 545 | /** 546 | * Get a string sequence with formatting: yyyyMMdd+code+seq 547 | */ 548 | public String nextSequence(String sequenceName) throws MysqlSequenceException { 549 | return nextSequence(sequenceName, null); 550 | } 551 | 552 | /** 553 | * Get a string sequence with formatting: yyyyMMdd+code+seq 554 | */ 555 | public String nextSequence(String sequenceName, String code) throws MysqlSequenceException { 556 | Counter counter = counters.computeIfAbsent(sequenceName, s -> new Counter(s, 0, 0)); 557 | Long nextLong = counter.next(); 558 | 559 | String today = DATE_FORMATTER.format(LocalDate.now()); 560 | boolean hasCode = code == null; 561 | SeqInfo seqInfo = seqInfoCache.get( 562 | sequenceName, () -> withConnection(conn -> querySeqInfo(conn, sequenceName, hasCode)) 563 | ); 564 | int length = (int) Math.log10(seqInfo.max) + 1; 565 | 566 | return today + (code != null ? code : seqInfo.code) + String.format("%0" + length + "d", nextLong); 567 | } 568 | 569 | ////////////////////////////////////////////////////////////// update functions 570 | 571 | public int updateValue(String sequenceName, long newValue) { 572 | String valueCol = this.columnInfoMap.get(Column.Value).value; 573 | String nameCol = this.columnInfoMap.get(Column.Name).value; 574 | return withConnection(connection -> execute(connection, 575 | "update " + this.tableName + " set " + valueCol + "=? where " + nameCol + "=?", 576 | newValue, sequenceName 577 | )); 578 | } 579 | 580 | public int updateStep(String sequenceName, long step) { 581 | String stepCol = this.columnInfoMap.get(Column.Step).value; 582 | String nameCol = this.columnInfoMap.get(Column.Name).value; 583 | return withConnection(connection -> execute(connection, 584 | "update " + this.tableName + " set " + stepCol + "=? where " + nameCol + "=?", 585 | step, sequenceName 586 | )); 587 | } 588 | 589 | ////////////////////////////////////////////////////////////// 590 | 591 | private String getColumnName(Column c) { 592 | return columnInfoMap.containsKey(c) ? columnInfoMap.get(c).value : null; 593 | } 594 | 595 | private ConnectionSupplier dataSource() { 596 | return ofNullable(this.connectionSupplier) 597 | .orElseThrow(() -> new IllegalStateException("connectionSupplier is null")); 598 | } 599 | 600 | private T withConnection(F f) throws MysqlSequenceException { 601 | try { 602 | Connection connection = dataSource().get(); 603 | 604 | if (connection == null) { 605 | throw new IllegalStateException("Connection is null"); 606 | } 607 | 608 | try { 609 | return f.f(connection); 610 | } finally { 611 | connectionCloser.close(connection); 612 | } 613 | } catch (Exception e) { 614 | throw new MysqlSequenceException(e); 615 | } 616 | } 617 | 618 | private void executeUpdateTemplate(Connection connection, String sequenceName) throws SQLException { 619 | execute(connection, this.updateTemplate, sequenceName); 620 | } 621 | 622 | private int execute(Connection connection, String sql, Object... params) throws SQLException { 623 | try (PreparedStatement ps = connection.prepareStatement(sql)) { 624 | for (int i = 0; i < params.length; i++) { 625 | Object param = params[i]; 626 | ps.setObject(i + 1, param); 627 | } 628 | return ps.executeUpdate(); 629 | } 630 | } 631 | 632 | private long[] querySegment(Connection connection, String sequenceName) throws SQLException { 633 | // System.out.println("开始取新的序列段..."); 634 | // __sleep__(1000); 635 | try (PreparedStatement ps = connection.prepareStatement(this.querySegmentTemplate)) { 636 | ps.setString(1, sequenceName); 637 | try (ResultSet rs = ps.executeQuery()) { 638 | if (rs.next()) { 639 | long sectionMin = rs.getBigDecimal(1).longValue(); 640 | long sectionMax = rs.getBigDecimal(2).longValue(); 641 | 642 | if (onSequenceUpdate != null) { 643 | try { 644 | onSequenceUpdate.accept(sectionMin, sectionMax); 645 | } catch (Throwable e) { 646 | // ignore event handler error 647 | } 648 | } 649 | 650 | return new long[]{sectionMin, sectionMax}; 651 | } 652 | } 653 | } 654 | throw new MysqlSequenceException("Sequence name '" + sequenceName + "' not found."); 655 | } 656 | 657 | private SeqInfo querySeqInfo(Connection connection, String seqName, boolean hasCode) { 658 | return seqInfoMap.computeIfAbsent(seqName, __seqName__ -> { 659 | // __sleep__(1000); 660 | try { 661 | try ( 662 | PreparedStatement ps = createPs(connection, this.queryCodeTemplate, __seqName__); 663 | ResultSet rs = ps.executeQuery() 664 | ) { 665 | if (rs.next()) { 666 | return new SeqInfo( 667 | hasCode ? rs.getString(getColumnName(Column.Code)) : null, 668 | rs.getLong(getColumnName(Column.Max)) 669 | ); 670 | } else { 671 | throw new MysqlSequenceException("Sequence name '" + seqName + "' not found."); 672 | } 673 | } 674 | } catch (SQLException e) { 675 | throw new MysqlSequenceException(e); 676 | } 677 | }); 678 | } 679 | 680 | private PreparedStatement createPs(Connection connection, String sql, Object... args) throws SQLException { 681 | PreparedStatement ps = connection.prepareStatement(sql); 682 | for (int i = 0; i < args.length; i++) { 683 | ps.setObject(i + 1, args[i]); 684 | } 685 | return ps; 686 | } 687 | 688 | ////////////////////////////////////////////////////////////// debugging 689 | 690 | private static void __sleep__(long millis) { 691 | try { 692 | Thread.sleep(millis); 693 | } catch (InterruptedException e) { 694 | // ignore this error 695 | } 696 | } 697 | 698 | private static void __output__(String message) { 699 | System.out.println(message + " [" + Thread.currentThread().getName() + "]"); 700 | } 701 | 702 | private static void __assert__(boolean b) { 703 | if (!b) { 704 | throw new IllegalStateException("Assert failed"); 705 | } 706 | } 707 | } 708 | -------------------------------------------------------------------------------- /src/main/java/com/hyd/mysqlsequencegenerator/MysqlSequenceGeneratorApplication.java: -------------------------------------------------------------------------------- 1 | package com.hyd.mysqlsequencegenerator; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.CommandLineRunner; 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.boot.autoconfigure.SpringBootApplication; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.jdbc.datasource.DataSourceUtils; 13 | 14 | import javax.sql.DataSource; 15 | import java.sql.Connection; 16 | import java.sql.SQLException; 17 | import java.util.Collections; 18 | 19 | /** 20 | * 示例:如何在 Spring Boot 项目中使用 MysqlSequenceGenerator 21 | */ 22 | @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") 23 | @SpringBootApplication 24 | public class MysqlSequenceGeneratorApplication { 25 | 26 | private static final Logger LOG = LoggerFactory.getLogger(MysqlSequenceGeneratorApplication.class); 27 | 28 | public static void main(String[] args) { 29 | SpringApplication.run(MysqlSequenceGeneratorApplication.class, args); 30 | } 31 | 32 | /////////////////////////////////////////////////////////////////// 示例:如何构建 MysqlSequenceGenerator 对象 33 | 34 | /** 35 | * 示例1:构建一个 MysqlSequenceGenerator 对象(附如何自定义表名) 36 | */ 37 | @Bean 38 | MysqlSequenceGenerator dataSourceSequenceGenerator( 39 | DataSource dataSource, 40 | @Value("${seq.table-name}") String tableName 41 | ) { 42 | return new MysqlSequenceGenerator( 43 | dataSource::getConnection, Connection::close, 44 | tableName, false, Collections.emptyList() 45 | ); 46 | } 47 | 48 | /** 49 | * 示例2:构建一个可以兼容 Spring 事务的 MysqlSequenceGenerator 对象 50 | * 当处于 Spring 事务中时,会使用当前已经获得的数据库连接,而不是再获取新的连接 51 | * 否则依然从 DataSource 中获取新的数据库连接 52 | */ 53 | @Bean 54 | MysqlSequenceGenerator inTransactionSequenceGenerator( 55 | DataSource dataSource 56 | ) { 57 | return new MysqlSequenceGenerator( 58 | () -> DataSourceUtils.getConnection(dataSource), 59 | conn -> DataSourceUtils.releaseConnection(conn, dataSource), 60 | null, false, Collections.emptyList() 61 | ); 62 | } 63 | 64 | //////////////////////////////////////////////////////////// 示例:如何使用 MysqlSequenceGenerator 对象 65 | 66 | /** 67 | * 如何使用 MysqlSequenceGenerator 对象 68 | */ 69 | @Autowired 70 | @Qualifier("dataSourceSequenceGenerator") 71 | private MysqlSequenceGenerator sequenceGenerator; 72 | 73 | @Bean 74 | CommandLineRunner commandLineRunner() { 75 | return args -> { 76 | initTable(); 77 | 78 | LOG.info("Update template: {}", sequenceGenerator.getUpdateTemplate()); 79 | LOG.info("Sequence: {}", sequenceGenerator.nextLong("seq1")); 80 | LOG.info("Sequence: {}", sequenceGenerator.nextLong("seq1")); 81 | LOG.info("Sequence: {}", sequenceGenerator.nextLong("seq1")); 82 | LOG.info("String Sequence: {}", sequenceGenerator.nextSequence("seq1")); 83 | LOG.info("String Sequence: {}", sequenceGenerator.nextSequence("seq1")); 84 | LOG.info("String Sequence: {}", sequenceGenerator.nextSequence("seq1")); 85 | }; 86 | } 87 | 88 | /////////////////////////////////////////////////////////////////// 89 | 90 | @Autowired 91 | private DataSource dataSource; 92 | 93 | private void initTable() throws SQLException { 94 | try (Connection connection = dataSource.getConnection()) { 95 | connection.createStatement().execute( 96 | "create database if not exists test" 97 | ); 98 | connection.createStatement().execute( 99 | "create table if not exists test.t_sequence(\n" + 100 | " name varchar(100) primary key comment '序列名称',\n" + 101 | " code varchar(5) not null default '00000' comment '序列编号(嵌入最终序列中)',\n" + 102 | " value bigint not null default 0 comment '当前值',\n" + 103 | " min bigint not null default 0 comment '最小值',\n" + 104 | " max bigint not null default 99999999 comment '最大值,当 value 达到最大值时重新从 min 开始',\n" + 105 | " step int not null default 1000 comment '步长,每次取序列时缓存多少,步长越大,数据库访问频率越低'\n" + 106 | ")" 107 | ); 108 | connection.createStatement().execute( 109 | "REPLACE into test.t_sequence (name, code, max) values ('seq1', '886', 99999999)" 110 | ); 111 | LOG.info("Table created."); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driver-class-name=com.mysql.jdbc.Driver 2 | spring.datasource.url=jdbc:mysql://localhost:3306/?useSSL=false&serverTimezone=UTC 3 | spring.datasource.username=root 4 | spring.datasource.password=root123 5 | 6 | seq.table-name=test.t_sequence -------------------------------------------------------------------------------- /src/test/java/com/hyd/mysqlsequencegenerator/MyCompanyTest.java: -------------------------------------------------------------------------------- 1 | package com.hyd.mysqlsequencegenerator; 2 | 3 | import com.hyd.mysqlsequencegenerator.MysqlSequenceGenerator.Column; 4 | import com.hyd.mysqlsequencegenerator.MysqlSequenceGenerator.ColumnInfo; 5 | import com.mysql.jdbc.Driver; 6 | import java.sql.Connection; 7 | import java.util.Arrays; 8 | import org.apache.commons.dbcp2.BasicDataSource; 9 | 10 | // 测试对公司环境的兼容性 11 | public class MyCompanyTest { 12 | 13 | public static void main(String[] args) { 14 | MysqlSequenceGenerator generator = createMysqlSequenceGenerator(); 15 | for (int i = 0; i < 110; i++) { 16 | System.out.println(generator.nextSequence("seq_t_fund_transfer_message", "041")); 17 | } 18 | } 19 | 20 | private static MysqlSequenceGenerator createMysqlSequenceGenerator() { 21 | 22 | // 准备数据源 23 | BasicDataSource basicDataSource = new BasicDataSource(); 24 | basicDataSource.setUrl("jdbc:mysql://172.16.10.40:3306/frxs_fund"); 25 | basicDataSource.setDriverClassName(Driver.class.getCanonicalName()); 26 | basicDataSource.setUsername("root"); 27 | basicDataSource.setPassword("123456"); 28 | 29 | // 构造 MysqlSequenceGenerator 对象 30 | MysqlSequenceGenerator mysqlSequenceGenerator = 31 | new MysqlSequenceGenerator( 32 | basicDataSource::getConnection, Connection::close, 33 | "t_sequence", false, 34 | Arrays.asList( 35 | ColumnInfo.customName(Column.Name, "name"), 36 | ColumnInfo.customName(Column.Value, "value"), 37 | ColumnInfo.customName(Column.Min, "min_value"), 38 | ColumnInfo.customName(Column.Max, "max_value"), 39 | ColumnInfo.customName(Column.Step, "step"), 40 | ColumnInfo.undefined(Column.Code) 41 | ) 42 | ); 43 | 44 | // 侦听序列更新事件 45 | mysqlSequenceGenerator.setOnSequenceUpdate((min, max) -> 46 | System.out.println(Thread.currentThread().getName() + " Sequence section updated: " + min + " ~ " + max)); 47 | 48 | return mysqlSequenceGenerator; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/hyd/mysqlsequencegenerator/MysqlSequenceGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.hyd.mysqlsequencegenerator; 2 | 3 | import com.mysql.jdbc.Driver; 4 | import org.apache.commons.dbcp2.BasicDataSource; 5 | import org.junit.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.concurrent.atomic.AtomicLong; 10 | 11 | public class MysqlSequenceGeneratorTest { 12 | 13 | public static final String URL = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC"; 14 | 15 | public static final String USERNAME = "root"; 16 | 17 | public static final String PASSWORD = "root123"; 18 | 19 | @Test 20 | public void testNextSequence() { 21 | MysqlSequenceGenerator mysqlSequenceGenerator = createMysqlSequenceGenerator(); 22 | for (int i = 0; i < 10; i++) { 23 | System.out.println(mysqlSequenceGenerator.nextSequence("seq1")); 24 | System.out.println(mysqlSequenceGenerator.nextSequence("seq2")); 25 | System.out.println(mysqlSequenceGenerator.nextSequence("seq3")); 26 | } 27 | } 28 | 29 | @Test 30 | public void testOnePerSection() throws Exception { 31 | MysqlSequenceGenerator mysqlSequenceGenerator = createMysqlSequenceGenerator(); 32 | mysqlSequenceGenerator.updateStep("seq1", 1); 33 | for (int i = 0; i < 100; i++) { 34 | System.out.println(mysqlSequenceGenerator.nextSequence("seq1")); 35 | } 36 | mysqlSequenceGenerator.updateStep("seq1", 100); 37 | } 38 | 39 | @Test 40 | public void benchmark() throws Exception { 41 | MysqlSequenceGenerator mysqlSequenceGenerator = createMysqlSequenceGenerator(); 42 | AtomicLong counter = new AtomicLong(0); 43 | 44 | Runnable task = () -> { 45 | for (int i = 0; i < 100000; i++) { 46 | try { 47 | mysqlSequenceGenerator.nextLong("seq1"); 48 | counter.incrementAndGet(); 49 | } catch (Exception e) { 50 | e.printStackTrace(); 51 | return; 52 | } 53 | } 54 | }; 55 | 56 | List threads = new ArrayList<>(); 57 | long start = System.currentTimeMillis(); 58 | 59 | for (int i = 0; i < 10; i++) { 60 | Thread thread = new Thread(task); 61 | thread.setName(String.format("Counter%02d", i)); 62 | threads.add(thread); 63 | thread.start(); 64 | } 65 | 66 | for (Thread thread : threads) { 67 | thread.join(); 68 | } 69 | 70 | long duration = System.currentTimeMillis() - start; 71 | System.out.println("count: " + counter.get() + ", duration: " + duration); 72 | } 73 | 74 | private MysqlSequenceGenerator createMysqlSequenceGenerator() { 75 | 76 | // 准备数据源 77 | BasicDataSource basicDataSource = new BasicDataSource(); 78 | basicDataSource.setUrl(URL); 79 | basicDataSource.setDriverClassName(Driver.class.getCanonicalName()); 80 | basicDataSource.setUsername(USERNAME); 81 | basicDataSource.setPassword(PASSWORD); 82 | 83 | // 构造 MysqlSequenceGenerator 对象 84 | MysqlSequenceGenerator generator = 85 | new MysqlSequenceGenerator(basicDataSource); 86 | 87 | // 侦听序列更新事件 88 | generator.setOnSequenceUpdate((min, max) -> { 89 | String threadName = Thread.currentThread().getName(); 90 | System.out.println("[" + threadName + "] Sequence segment updated: " + min + " ~ " + max); 91 | }); 92 | 93 | return generator; 94 | } 95 | } --------------------------------------------------------------------------------