Replacing UUID Primary Keys with a Custom Snowflake ID Generator in Spring Boot 3.3
The article explains why UUIDv4 primary keys hurt performance and storage, then demonstrates how to replace them in a Spring Boot 3.3 application with a custom Snowflake‑based 64‑bit ID generator, providing full Maven setup, configuration, entity, service, REST controller, and Thymeleaf UI example.
UUID (Universally Unique Identifier) is a widely used 128‑bit identifier. UUIDv4, based on random numbers, is common in distributed systems but brings performance issues, larger storage consumption, and inefficient sorting when used as a database primary key.
This article introduces the concept and drawbacks of UUID and shows how to replace UUIDv4 with a custom ID generation strategy in Spring Boot 3.3.
Custom ID Generation Strategy
We adopt an improved Snowflake algorithm that generates ordered, unique 64‑bit IDs, which are more suitable for high‑concurrency environments.
Database Table DDL
CREATE TABLE user (
id BIGINT NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL
);Project Configuration (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>3.3.3</version>
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>custom-idgenerator</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<mybatis-plus-boot-starter.version>3.5.7</mybatis-plus-boot-starter.version>
<mybatis-spring.version>3.0.3</mybatis-spring.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis‑Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok (optional) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Application Configuration (application.yml)
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
app:
snowflake:
workerId: 1
dataCenterId: 1Configuration Class
@Data
@Component
@ConfigurationProperties(prefix = "app.snowflake")
public class SnowflakeProperties {
private long workerId;
private long dataCenterId;
}Snowflake ID Generator Implementation
package com.icoderoad.customidgenerator.generator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.icoderoad.customidgenerator.config.SnowflakeProperties;
@Component
public class SnowflakeIdGenerator {
private static final long TW_EPOCH = 1288834974657L;
private static final long WORKER_ID_BITS = 5L;
private static final long DATA_CENTER_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
private final long workerId;
private final long dataCenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
@Autowired
public SnowflakeIdGenerator(SnowflakeProperties properties) {
if (properties.getWorkerId() > MAX_WORKER_ID || properties.getWorkerId() < 0) {
throw new IllegalArgumentException("Worker ID must be between 0 and " + MAX_WORKER_ID);
}
if (properties.getDataCenterId() > MAX_DATA_CENTER_ID || properties.getDataCenterId() < 0) {
throw new IllegalArgumentException("Data Center ID must be between 0 and " + MAX_DATA_CENTER_ID);
}
this.workerId = properties.getWorkerId();
this.dataCenterId = properties.getDataCenterId();
}
public synchronized long generateId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - TW_EPOCH) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}Entity Definition (User)
@Data
public class User {
@TableId(type = IdType.INPUT) // manual ID input
private Long id;
private String name;
private String email;
}Mapper Layer
@Mapper
public interface UserMapper extends BaseMapper
{}Service Layer
public interface UserService extends IService
{
void createUser(String name, String email);
}Service Implementation
@Service
public class UserServiceImpl extends ServiceImpl
implements UserService {
@Autowired
private SnowflakeIdGenerator idGenerator;
@Override
public void createUser(String name, String email) {
User user = new User();
user.setId(idGenerator.generateId());
user.setName(name);
user.setEmail(email);
save(user);
}
}REST Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity
> getAllUsers() {
List
users = userService.list();
return new ResponseEntity<>(users, HttpStatus.OK);
}
@PostMapping
public ResponseEntity
createUser(@RequestParam String name, @RequestParam String email) {
try {
userService.createUser(name, email);
return new ResponseEntity<>("用户创建成功", HttpStatus.CREATED);
} catch (Exception e) {
return new ResponseEntity<>("用户创建失败", HttpStatus.BAD_REQUEST);
}
}
}Thymeleaf Front‑End (index.html)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container mt-5">
<h2>用户管理</h2>
<div id="message" class="alert" style="display:none;">
<form id="userForm">
<div class="mb-3">
<label for="name" class="form-label">姓名</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">电子邮箱</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<button type="submit" class="btn btn-primary">创建用户</button>
</form>
<h3 class="mt-5">用户列表</h3>
<table class="table table-striped mt-4">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>电子邮箱</th>
</tr>
</thead>
<tbody id="userTableBody"><!-- Filled by jQuery --></tbody>
</table>
</div>
<script>
$(document).ready(function() {
function loadUserList() {
$.getJSON('/api/users', function(data) {
var rows = '';
$.each(data, function(index, user) {
rows += '<tr>';
rows += '<td>' + user.id + '</td>';
rows += '<td>' + user.name + '</td>';
rows += '<td>' + user.email + '</td>';
rows += '</tr>';
});
$('#userTableBody').html(rows);
});
}
loadUserList();
$('#userForm').on('submit', function(e) {
e.preventDefault();
var formData = $(this).serialize();
$.post('/api/users', formData, function(response) {
$('#message').removeClass('alert-danger').addClass('alert-success')
.text('用户创建成功!').show();
$('#userForm')[0].reset();
loadUserList();
}).fail(function() {
$('#message').removeClass('alert-success').addClass('alert-danger')
.text('用户创建失败,请重试。').show();
});
});
});
</script>
</body>
</html>By replacing UUIDv4 with the Snowflake‑based custom ID generator, the application achieves better insert performance, lower storage overhead, and more efficient sorting. The article provides a complete end‑to‑end example, covering database schema, Maven configuration, Spring Boot beans, service logic, REST API, and a simple Thymeleaf front‑end.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.