需求说明

产品要求实现一个订单编号,此编号规则如下

订单编号规则:

“字母” + “日期” + “自增ID”

订单编号举例

比如业务A,在2020-08-04日有三个订单,那么订单编号如下:

  • A202008040001
  • A202008040002
  • A202008040003

比如业务A,在2020-08-05日有4个订单,那么订单编号如下:

  • A202008050001
  • A202008050002
  • A202008050003
  • A202008050003

通过上面的例子可以看到,后面的“自增ID”每天都会从1开始增加,在一个分布式系统中,要做到每天从1开始不重复并且自增的效果;想到的第一个实现方案就是redis的Incr命令(Redis Incr 命令将 key 中储存的数字值增一)。

需求实现

配置redis

依赖redis相关jar包

因为此模块继承了spring-boot-starter-parent,所以不需要指定版本

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>redis.clients</groupId>
  7. <artifactId>jedis</artifactId>
  8. </dependency>

编写配置redis的config

  1. package com.nicai.config;
  2. import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
  3. import org.springframework.boot.context.properties.EnableConfigurationProperties;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.data.redis.connection.RedisClusterConfiguration;
  7. import org.springframework.data.redis.connection.RedisConnectionFactory;
  8. import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
  9. import org.springframework.data.redis.core.StringRedisTemplate;
  10. /**
  11. * redis集群配置
  12. *
  13. * @author guozhe
  14. * @date 2020/08/04
  15. */
  16. @Configuration
  17. @EnableConfigurationProperties(RedisProperties.class)
  18. public class RedisClusterConfig {
  19. private final RedisProperties redisProperties;
  20. public RedisClusterConfig(RedisProperties redisProperties) {
  21. this.redisProperties = redisProperties;
  22. }
  23. /**
  24. * Thread-safe factory of Redis connections配置
  25. *
  26. * @return factory of Redis
  27. */
  28. @Bean
  29. public RedisConnectionFactory redisConnectionFactory() {
  30. RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
  31. redisClusterConfiguration.setPassword(redisProperties.getPassword());
  32. return new JedisConnectionFactory(redisClusterConfiguration);
  33. }
  34. /**
  35. * 创建String类型的redis模板
  36. *
  37. * @param redisConnectionFactory factory of Redis
  38. * @return String-focused extension of RedisTemplate
  39. */
  40. @Bean
  41. public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
  42. StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory);
  43. template.afterPropertiesSet();
  44. return template;
  45. }
  46. }

如果是配置范型的RedisTemplate,需要设置值的序列化规则为:StringRedisSerializer,原因可以参考此文章:Spring Boot中使用RedisTemplate优雅的操作Redis,并且解决RedisTemplate泛型注入失败的问题

测试redis的config代码

  1. package com.nicai.config;
  2. import com.yuanfeng.accounting.BaseAdminSpringTest;
  3. import com.yuanfeng.accounting.Constants;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.junit.Assert;
  6. import org.junit.Test;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.data.redis.core.StringRedisTemplate;
  9. import org.springframework.data.redis.core.ValueOperations;
  10. import java.util.concurrent.TimeUnit;
  11. /**
  12. * @author guozhe
  13. * @date 2020/08/04
  14. */
  15. @Slf4j
  16. @RunWith(SpringRunner.class)
  17. @SpringBootTest(classes = AdminApplication.class)
  18. public class RedisClusterConfigTest {
  19. private static final String TEST_KEY = Constants.REDIS_KEY_PREFIX + "test:hello";
  20. private static final String TEST_VALUE = "world";
  21. @Autowired
  22. private StringRedisTemplate stringRedisTemplate;
  23. @Test
  24. public void testStringRedisTemplateGetAndSet() {
  25. stringRedisTemplate.opsForValue().set(TEST_KEY, TEST_VALUE);
  26. String value = stringRedisTemplate.opsForValue().get(TEST_KEY);
  27. Assert.assertEquals(TEST_VALUE, value);
  28. stringRedisTemplate.delete(TEST_KEY);
  29. Assert.assertNull(stringRedisTemplate.opsForValue().get(TEST_KEY));
  30. }
  31. @Test
  32. public void testIncr() {
  33. String key = TEST_KEY;
  34. ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
  35. valueOperations.set(key, "1", 24, TimeUnit.HOURS);
  36. String initValue = valueOperations.get(key);
  37. log.info("key={}, init value={}", key, initValue);
  38. Assert.assertEquals("1", initValue);
  39. Long increment = valueOperations.increment(key);
  40. log.info("key={}, after increment={}", key, increment);
  41. Assert.assertEquals(Long.valueOf(2), increment);
  42. stringRedisTemplate.delete(key);
  43. Assert.assertNull(valueOperations.get(key));
  44. }
  45. }

基于redis编写唯一ID生成服务

添加抽象的唯一id生成服务

  1. package com.nicai.service;
  2. import cn.hutool.core.util.BooleanUtil;
  3. import com.alibaba.fastjson.JSON;
  4. import lombok.AllArgsConstructor;
  5. import lombok.Data;
  6. import lombok.NoArgsConstructor;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.data.redis.core.StringRedisTemplate;
  9. import java.util.Objects;
  10. import java.util.Optional;
  11. import java.util.concurrent.TimeUnit;
  12. /**
  13. * 分布式ID生成服务
  14. *
  15. * @author guozhe
  16. * @date 2020/08/04
  17. */
  18. @Slf4j
  19. public abstract class AbstractRedisDistributedIDGenerateService<T extends AbstractRedisDistributedIDGenerateService.Context> {
  20. /**
  21. * 初始化key时的默认值
  22. */
  23. private static final long DEFAULT_VALUE = 0;
  24. protected final StringRedisTemplate redisTemplate;
  25. public AbstractRedisDistributedIDGenerateService(StringRedisTemplate redisTemplate) {
  26. this.redisTemplate = redisTemplate;
  27. }
  28. /**
  29. * 获取下一个ID,直接从redis中获取自增后的值;
  30. *
  31. * @return 下一个ID, 如果redis出现异常则返回null,请使用者自行处理
  32. */
  33. public final Optional<Long> nextId() {
  34. // 从redis中获取自增id
  35. Long id = incr(getKey());
  36. return Objects.isNull(id) ? Optional.empty() : Optional.of(id);
  37. }
  38. /**
  39. * 获取下一个ID,根据传入的上下文和redis中自增后的值最终组装成下一个ID;
  40. * 获取之后会交给子类检查此ID是否重复,如果重复会从子类中获取最新的ID,然后更新redis中的值
  41. *
  42. * @param context 拼装id时需要的上下文
  43. * @return 下一个ID
  44. */
  45. public final String nextId(T context) {
  46. Optional<Long> id = nextId();
  47. // 如果可以从redis中获取值,则说明redis服务正常,需要判重;否则直接从数据库中获取下一个id
  48. String nextId = id.isPresent() ? ifDuplicatedThenUpdate(context, assemblyNextId(context, id.get())) :
  49. getNewIdFromDbAndUpdateRedis(context, null);
  50. if (log.isDebugEnabled()) {
  51. log.debug("context={},redisIncrId={} nextId={}", JSON.toJSONString(context), id, nextId);
  52. }
  53. return nextId;
  54. }
  55. /**
  56. * 检查获取到的ID是否重复
  57. * 如果重复则说明由于redis的一些原因导致的重复,返回最新的redis中应该存在的值
  58. *
  59. * @param nextId 下一个ID
  60. * @return 如果当前ID没有重复,则返回null,否则如果重复了则返回redis中应该有的值
  61. */
  62. protected abstract boolean checkIfDuplicated(String nextId);
  63. /**
  64. * 从数据库获取下一个id
  65. *
  66. * @param duplicatedId 重复的id,此入参可能为null,子类需要自己处理
  67. * @return 数据库获取下一个id
  68. */
  69. protected abstract Long maxIdFromDatabase(String duplicatedId);
  70. /**
  71. * 子类根据redis当前的值自行组装最终的ID
  72. *
  73. * @param context 上下文
  74. * @param redisValue redis当前的值
  75. * @return 最终的ID
  76. */
  77. protected abstract String assemblyNextId(T context, Long redisValue);
  78. /**
  79. * 获取redis自增的key
  80. *
  81. * @return redis自增的key
  82. */
  83. protected abstract String getKey();
  84. /**
  85. * 调用redis的自增方法
  86. * 如果key不存在则先设置key,再调用自增方法
  87. *
  88. * @param key 需要自增的key
  89. * @return 自增之后的值,如果redis出现异常则返回null
  90. */
  91. Long incr(String key) {
  92. Long increment = null;
  93. try {
  94. // 先检查redis中是否有key,如果没有,先设置key并且设置过期时间
  95. if (BooleanUtil.isFalse(redisTemplate.hasKey(key))) {
  96. initOrUpdateValue(key, getKeyInitValue());
  97. }
  98. increment = redisTemplate.opsForValue().increment(key);
  99. } catch (Exception e) {
  100. log.error("调用redis的自增方法异常,error_message={}", e.getMessage(), e);
  101. }
  102. log.debug("key = {}, increment={}", key, increment);
  103. return increment;
  104. }
  105. /**
  106. * 获取初始化key时的value值,默认是0,自增之后id从1开始;
  107. * 如果子类想从其他数字开始则自己覆盖此方法即可
  108. *
  109. * @return 初始化key时的value值
  110. */
  111. protected long getKeyInitValue() {
  112. return DEFAULT_VALUE;
  113. }
  114. /**
  115. * 获取key的超时时间,单位是小时,由子类设置
  116. *
  117. * @return 超时时间,单位小时
  118. */
  119. protected abstract long getTimeOutHours();
  120. /**
  121. * 判断是否重复,如果重复则从别的渠道(由子类自己决定从哪个渠道)更新
  122. *
  123. * @param context 拼装id时需要的上下文
  124. * @param nextId 下一个id
  125. * @return 如果重复则返回新的nextId,否则返回入参传入的nextId
  126. */
  127. private String ifDuplicatedThenUpdate(T context, String nextId) {
  128. // 判断是否重复,如果重复则从数据库中获取,否则直接返回当前值
  129. return checkIfDuplicated(nextId) ? getNewIdFromDbAndUpdateRedis(context, nextId) : nextId;
  130. }
  131. /**
  132. * 从数据库获取新id并更新redis中的值
  133. *
  134. * @param context 拼装id时需要的上下文
  135. * @param nextId 下一个id
  136. * @return 根据数据库的id获得的新id
  137. */
  138. private String getNewIdFromDbAndUpdateRedis(T context, String nextId) {
  139. Long maxIdFromDatabase = maxIdFromDatabase(nextId);
  140. String newId = assemblyNextId(context, maxIdFromDatabase);
  141. log.warn("nextId={} 在数据库中已经存在,maxIdFromDatabase={} 重新获取新的newId={}", nextId, maxIdFromDatabase, newId);
  142. initOrUpdateValue(getKey(), maxIdFromDatabase);
  143. return newId;
  144. }
  145. /**
  146. * 初始化或者更新redis中的自增的值
  147. *
  148. * @param key redis中的key
  149. * @param value 需要设置的值
  150. */
  151. private void initOrUpdateValue(String key, Long value) {
  152. try {
  153. redisTemplate.opsForValue().set(key, String.valueOf(value), getTimeOutHours(), TimeUnit.HOURS);
  154. } catch (Exception e) {
  155. log.error("设置redis值异常,value={} error_message={}", value, e.getMessage(), e);
  156. }
  157. }
  158. /**
  159. * 上下文;子类自己定义上下文,然后根据上下文的数据来最终组装ID
  160. */
  161. public interface Context {
  162. }
  163. /**
  164. * 凭证编号上下文
  165. */
  166. @Data
  167. @NoArgsConstructor
  168. @AllArgsConstructor
  169. public static class AContext implements Context {
  170. /**
  171. * 业务类型
  172. */
  173. private String businessType;
  174. }
  175. }

添加一个A服务的唯一id生成服务实现

  1. package com.nicai.service.impl;
  2. import cn.hutool.core.date.DatePattern;
  3. import cn.hutool.core.date.DateUtil;
  4. import cn.hutool.core.util.StrUtil;
  5. import com.yuanfeng.accounting.Constants;
  6. import com.yuanfeng.accounting.dao.ManualVoucherDAO;
  7. import com.yuanfeng.accounting.entity.ManualVoucherEntity;
  8. import com.yuanfeng.accounting.exception.AccountingException;
  9. import com.yuanfeng.accounting.service.AbstractRedisDistributedIDGenerateService;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.springframework.data.redis.core.StringRedisTemplate;
  12. import org.springframework.stereotype.Service;
  13. import java.util.List;
  14. import java.util.Objects;
  15. import java.util.Optional;
  16. /**
  17. * 分布式唯一ID生成-A实现类
  18. * 编号规则:用途+日期+自增ID,如:A202007310001;A202007310002;A202008070001;
  19. *
  20. * @author guozhe
  21. * @date 2020/08/04
  22. */
  23. @Slf4j
  24. @Service
  25. public class DistributedIDGenerateServiceAImpl extends AbstractRedisDistributedIDGenerateService<AbstractRedisDistributedIDGenerateService.AContext> {
  26. /**
  27. * 业务类型
  28. */
  29. private static final String BUSINESS_TYPE = "A:";
  30. /**
  31. * ID长度不足4位时在前面填充的字符
  32. */
  33. private static final char FILLED_CHAR = '0';
  34. /**
  35. * 最后的自增ID的长度
  36. */
  37. private static final int INCREMENT_LENGTH = 4;
  38. /**
  39. * 过期小时数,即在24小时候过期
  40. */
  41. private static final int EXPIRATION_HOURS = 24;
  42. public DistributedIDGenerateServiceAImpl(StringRedisTemplate redisTemplate) {
  43. super(redisTemplate);
  44. }
  45. @Override
  46. protected boolean checkIfDuplicated(String nextId) {
  47. return false;
  48. }
  49. @Override
  50. protected Long maxIdFromDatabase(String duplicatedId) {
  51. return 1L;
  52. }
  53. @Override
  54. protected String assemblyNextId(VoucherNumberContext context, Long redisValue) {
  55. return String.join(Constants.BLANK, context.getBusinessType(), getDatePeriod(),
  56. StrUtil.fillBefore(String.valueOf(redisValue), FILLED_CHAR, INCREMENT_LENGTH));
  57. }
  58. @Override
  59. protected String getKey() {
  60. return String.join(Constants.REDIS_KEY_DELIMITER, Constants.REDIS_KEY_PREFIX, BUSINESS_TYPE, getDatePeriod());
  61. }
  62. @Override
  63. protected long getTimeOutHours() {
  64. return EXPIRATION_HOURS;
  65. }
  66. }