springboot-stater + redis + lua 實現一個簡單的發號器(3)-- 實現篇

zero發表於2021-12-09

接著上一篇 php + redis + lua 實現一個簡單的發號器(1)-- 原理篇,本篇講一下spring-boot-starter 版本的發號器的具體實現。✋github地址?歡迎start、clone

1、基礎知識

發號器的實現主要用到了下面的一些知識點:

1. php中的位運算的操作和求值

2. 計算機原碼、補碼、反碼的基本概念

3. redis中lua指令碼的編寫和除錯

4. 如何自己定一個spring-boot-starter

2、具體實現

整個starter的目錄結構如下

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── srorders
│   │   │           └── starter
│   │   │               ├── SignGenerator.java
│   │   │               ├── UuidConfiguration.java
│   │   │               └── UuidProperties.java
│   │   └── resources
│   │       ├── META-INF
│   │       │   └── spring.factories
│   │       └── application.yml
│   └── test
│       └── java
│           └── com
│               └── srorders
│                   └── starter
│                       └── SignGeneratorTest.java

pom的相關依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.srorders.starter</groupId>
    <artifactId>uuid</artifactId>
    <version>1.0.0</version>
    <name>uuid</name>
    <description>uuid</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--Spring Boot Starter: START-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 引入redis的starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <!-- Import dependency management from Spring Boot -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.6.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

定義一個spring-boot-starter主要分為4個部分:

1、定義一個發號器服務

package com.srorders.starter;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

public class SignGenerator {

    /**
     * 申請64位記憶體
     */
    public static final int BITS_FULL = 64;

    /**
     * uuid
     */
    public static final String BITS_FULL_NAME = "id";

    /**
     * 1位符號位
     */
    public static final int BITS_PREFIX = 1;

    /**
     * 41時間位
     */
    public static final int BITS_TIME = 41;

    /**
     * 時間位名稱
     */
    public static final String BITS_TIME_NAME = "diffTime";

    /**
     * 產生的時間
     */
    public static final String BITS_GENERATE_TIME_NAME = "generateTime";

    /**
     * 5個伺服器位
     */
    public static final int BITS_SERVER = 5;

    /**
     * 服務位名稱
     */
    public static final String BITS_SERVER_NAME = "serverId";

    /**
     * 5個worker位
     */
    public static final int BITS_WORKER = 5;

    /**
     * worker位名稱
     */
    public static final String BITS_WORKER_NAME = "workerId";

    /**
     * 12個自增位
     */
    public static final int BITS_SEQUENCE = 12;

    /**
     * 自增位名稱
     */
    public static final String BITS_SEQUENCE_NAME = "sequenceNumber";


    /**
     * uuid配置
     */
    private UuidProperties uuidProperties;

    /**
     * redis client
     */
    private StringRedisTemplate redisTemplate;

    /**
     * 構造
     *
     * @param uuidProperties
     */
    public SignGenerator(UuidProperties uuidProperties, StringRedisTemplate redisTemplate) {
        this.uuidProperties = uuidProperties;
        this.redisTemplate = redisTemplate;
    }

    private long getStaterOffsetTime() {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return LocalDateTime.parse(uuidProperties.getOffsetTime(), dateTimeFormatter).toInstant(OffsetDateTime.now().getOffset())
                .toEpochMilli();
    }

    /**
     * 獲取uuid
     *
     * @return
     */
    public Map<String, Long> getNumber() throws InterruptedException {
        HashMap<String, Long> result = new HashMap<>();
        do {
            long id = 0L;
            long diffTime = Instant.now().toEpochMilli() - this.getStaterOffsetTime();
            long maxDiffTime = (long) (Math.pow(2, BITS_TIME) - 1);

            if (diffTime > maxDiffTime) {
                throw new RuntimeException(String.format("the offsetTime: %s is too small", uuidProperties.getOffsetTime()));
            }

            // 對時間位進行計算
            int shift = BITS_FULL - BITS_PREFIX - BITS_TIME;
            id |= diffTime << shift;
            result.put(BITS_TIME_NAME, diffTime);

            // 對server進行計算
            shift = shift - BITS_SERVER;
            id |= uuidProperties.getServerId() << shift;
            result.put(BITS_SERVER_NAME, uuidProperties.getServerId());

            // 對worker進行計算
            shift = shift - BITS_WORKER;
            id |= uuidProperties.getWorkerId() << shift;
            result.put(BITS_WORKER_NAME, uuidProperties.getWorkerId());

            // 對sequence進行計算
            Long sequence = this.getSequence("uuid_" + diffTime);
            long maxSequence = (long) (Math.pow(2, BITS_SEQUENCE) - 1);
            if (sequence > maxSequence) {
                Thread.sleep(1);
            } else {
                id |= sequence;
                result.put(BITS_SEQUENCE_NAME, sequence);
                result.put(BITS_FULL_NAME, id);
                return result;
            }
        } while (true);
    }

    /**
     * 獲取自增id
     *
     * @param id
     * @return
     */
    private Long getSequence(String id) {
        String lua = " local sequenceKey = KEYS[1]; " +
                "local sequenceNumber = redis.call(\"incr\", sequenceKey); " +
                "redis.call(\"pexpire\", sequenceKey, 100); " +
                "return sequenceNumber";
        RedisScript<Long> redisScript = RedisScript.of(lua, Long.class);
        return redisTemplate.execute(redisScript, Collections.singletonList(id));
    }

    /**
     * 反解id
     *
     * @param id
     * @return
     */
    public Map<String, Long> reverseNumber(Long id) {
        HashMap<String, Long> result = new HashMap<>();

        //time
        int shift = BITS_FULL - BITS_PREFIX - BITS_TIME;
        Long diffTime = (id >> shift) & (long) (Math.pow(2, BITS_TIME) - 1);
        result.put(BITS_TIME_NAME, diffTime);

        //generateTime
        Long generateTime = diffTime + this.getStaterOffsetTime();
        result.put(BITS_GENERATE_TIME_NAME, generateTime);

        //server
        shift = shift - BITS_SERVER;
        Long server = (id >> shift) & (long) (Math.pow(2, BITS_SERVER) - 1);
        result.put(BITS_SERVER_NAME, server);

        //worker
        shift = shift - BITS_WORKER;
        Long worker = (id >> shift) & (long) (Math.pow(2, BITS_WORKER) - 1);
        result.put(BITS_WORKER_NAME, worker);

        //sequence
        Long sequence = id & (long) (Math.pow(2, BITS_SEQUENCE) - 1);
        result.put(BITS_SEQUENCE_NAME, sequence);
        return result;
    }
}

2、定義一個產生bean的自動配置類

package com.srorders.starter;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * @author zero
 */
@Configuration
@EnableConfigurationProperties(UuidProperties.class)
@ConditionalOnClass({StringRedisTemplate.class, UuidProperties.class})
public class UuidConfiguration {
   @Bean
   @ConditionalOnMissingBean(SignGenerator.class)
   public SignGenerator signGenerator(UuidProperties  uuidProperties, StringRedisTemplate stringRedisTemplate) {
       return new SignGenerator(uuidProperties, stringRedisTemplate);
   }
}

3、定義一個對映application.properties(application.yml)配置的物件

package com.srorders.starter;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author zero
 */
@ConfigurationProperties("spring.uuid")
@Data
public class UuidProperties {

    private Long serverId = 0L;

    private Long workerId = 0L;

    private String offsetTime = "2021-12-07 00:00:00";
}

4、在resources目錄下建立 META-INF 目錄,然後在該目錄下建立 spring.factories 檔案,內容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.srorders.starter.UuidConfiguration

3、執行一把

1、建立一個簡單的spring-boot-web專案, pom.xml 檔案內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.srorders.spring.boot</groupId>
    <artifactId>starter-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>starter-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

        <!-- 這裡我們引入我們自定義的 starter -->
        <dependency>
            <groupId>com.srorders.starter</groupId>
            <artifactId>uuid</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2、新建一個控制器內容如下

package com.srorders.spring.boot.controller;

import com.srorders.starter.SignGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;

@RestController
public class UuidController {

    @Autowired
    SignGenerator signedService;

    @GetMapping("/getUuid")
    public String getUuid() throws InterruptedException {
        return this.signedService.getNumber().get(SignGenerator.BITS_FULL_NAME).toString();
    }

    @GetMapping("/reverse")
    public Map<String,Long> reverse(@RequestParam(value = "id") Long id) throws InterruptedException {
        return this.signedService.reverseNumber(id);
    }
}

相關文章