├── .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 | }
--------------------------------------------------------------------------------