一、基础设施环境快速搭建
在开始构建会员体系前,必须先准备好数据库与缓存环境。为了确保环境的一致性和可复现性,推荐使用Docker进行部署。以下命令可直接在Linux服务器或本地终端执行,拉取MySQL 8.0和Redis 6.0镜像并启动。
1. 部署MySQL数据库
执行以下命令创建MySQL容器,设置root密码为Member@123,并初始化字符集为utf8mb4,防止中文乱码:
```bash
docker run -d \
--name member-mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=Member@123 \
-e MYSQL_DATABASE=member_db \
mysql:8.0 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci
```
2. 部署Redis缓存
执行以下命令启动Redis服务,禁用持久化以提升本次实操的读写性能,并设置最大内存为2GB:
```bash
docker run -d \
--name member-redis \
-p 6379:6379 \
redis:6.0-alpine \
redis-server --appendonly no --maxmemory 2gb
```
二、会员库分库分表设计与落地
当会员量突破千万级时,单表查询性能会急剧下降。我们需要采用水平分表策略。这里使用user_id作为分片键,取模运算将数据分散到10张表中。
1. 建表SQL语句
请在MySQL客户端执行以下脚本。这里创建member_info_0到member_info_9共10张表。包含user_id(主键)、points(积分)、level(等级)和update_time(更新时间)。注意,user_id必须设为BigInt以容纳雪花算法生成的长ID。
```sql
CREATE DATABASE IF NOT EXISTS member_db;
USE member_db;
-- 循环建表逻辑(此处展示手动建表前两张,实际需通过脚本生成0-9)
CREATE TABLE IF NOT EXISTS member_info_0 (
user_id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
points BIGINT DEFAULT 0,
level TINYINT DEFAULT 1,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_level (level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS member_info_1 (
user_id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
points BIGINT DEFAULT 0,
level TINYINT DEFAULT 1,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_level (level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
2. 分片路由算法逻辑
在代码层实现简单的路由算法。假设获取到user_id,计算目标表名的逻辑如下:
```java
public String getTableName(Long userId) {
int tableIndex = (int) (userId % 10);
return "member_info_" + tableIndex;
}
```
三、分布式唯一ID生成实现
分库分表后,不能依赖数据库自增ID。我们需要实现一个简单的雪花算法(Snowflake)来生成全局唯一的user_id。以下是基于Java的实现,包含时间戳、机器ID和序列号。
1. 雪花算法工具类
将此类直接复制到项目中。设置workerId为1(实际生产环境需通过配置文件读取不同机器的ID)。
```java
public class SnowflakeIdGenerator {
private final long twepoch = 1288834974657L;
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long sequenceBits = 12L;
private final long workerIdShift = sequenceBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
}
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards.");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
}
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
protected long timeGen() {
return System.currentTimeMillis();
}
}
```
四、高并发积分操作原子性方案
会员积分变更(如签到、消费)是高频操作。直接更新数据库会导致行锁竞争严重。必须使用Redis进行原子计数,并异步刷盘。核心难点在于保证“查询-判断-扣减”的原子性,这里使用Lua脚本解决。

1. Redis Lua脚本:积分扣减
创建文件deduct_points.lua。该脚本接收Key和扣减金额,先检查余额是否充足,充足则扣减并返回1,不足返回0。
```lua
local key = KEYS[1]
local deduct_val = tonumber(ARGV[1])
local current_val = tonumber(redis.call('GET', key))
if current_val == nil then
return 0
end
if current_val >= deduct_val then
return redis.call('DECRBY', key, deduct_val)
else
return -1
end
```
2. Java调用Lua脚本示例
使用Jedis或Lettuce加载并执行上述脚本。确保脚本执行是原子的,不会被其他线程插队。
```java
// 假设 Jedis jedis = ...; String script = ...;
public boolean deductPoints(Long userId, int points) {
String key = "member:points:" + userId;
// 加载脚本内容
String script = "local key = KEYS[1] local deduct_val = tonumber(ARGV[1]) local current_val = tonumber(redis.call('GET', key)) if current_val == nil then return 0 end if current_val >= deduct_val then return redis.call('DECRBY', key, deduct_val) else return -1 end";
Object result = jedis.eval(script, 1, key, String.valueOf(points));
long newPoints = Long.parseLong(result.toString());
if (newPoints >= 0) {
// 成功扣减,发送消息到队列异步更新MySQL
sendToQueue(userId, newPoints);
return true;
}
return false;
}
```
五、Redis数据异步持久化策略
为了性能,Redis是前置存储,MySQL是最终存储。我们不能在扣减积分时同步写MySQL。需要引入一个轻量级的消息队列(如RocketMQ或Kafka,甚至内存队列)进行异步削峰填谷。
1. 消息实体定义
```java
public class PointsEvent {
private Long userId;
private Long points;
private Date updateTime;
// getters and setters
}
```
2. 消费者批量更新MySQL
消费者监听队列,积累一批数据(如100条或每5秒)后,执行一次批量UPDATESQL。注意要根据user_id路由到对应的分表。
```java
// 伪代码:批量更新逻辑
public void consumeBatchUpdate(List
events) {
// 按分表分组
Map> groupedEvents = new HashMap<>();
for (PointsEvent event : events) {
String tableName = getTableName(event.getUserId());
groupedEvents.computeIfAbsent(tableName, k -> new ArrayList<>()).add(event);
}
// 遍历每个表执行批量更新
for (Map.Entry> entry : groupedEvents.entrySet()) {
String tableName = entry.getKey();
List list = entry.getValue();
StringBuilder sql = new StringBuilder("UPDATE ").append(tableName).append(" SET points = CASE user_id ");
List userIds = new ArrayList<>();
for (PointsEvent e : list) {
sql.append(" WHEN ? THEN ? ");
userIds.add(e.getUserId());
userIds.add(e.getPoints());
}
sql.append(" END WHERE user_id IN (");
// 拼接占位符
String placeholders = list.stream().map(e -> "?").collect(Collectors.joining(","));
sql.append(placeholders).append(")");
// 执行JDBC批量更新
// jdbcTemplate.update(sql.toString(), params...);
}
}
```
六、会员等级升降级触发器
会员等级通常由积分总额决定。为了避免每次积分变动都计算等级,我们采用“延迟计算”或“定时任务”策略。这里提供一种基于MySQL事件调度器的自动化方案,每天凌晨自动重算所有会员等级。
1. 开启MySQL事件调度器
```sql
SET GLOBAL event_scheduler = ON;
```
2. 创建等级更新存储过程
该存储过程遍历所有分表,根据积分更新等级:>10000为金牌,>5000为银牌,其他为普通。
```sql
DELIMITER $$
CREATE PROCEDURE batch_update_member_level()
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE table_name VARCHAR(20);
WHILE i < 10 DO
SET table_name = CONCAT('member_info_', i);
SET @sql = CONCAT('
UPDATE ', table_name, '
SET level = CASE
WHEN points >= 10000 THEN 3
WHEN points >= 5000 THEN 2
ELSE 1
END
');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET i = i + 1;
END WHILE;
END$$
DELIMITER ;
```
3. 创建每日定时事件
设置每天凌晨2:00执行一次等级更新。
```sql
CREATE EVENT IF NOT EXISTS daily_level_check
ON SCHEDULE EVERY 1 DAY
STARTS (TIMESTAMP(CURRENT_DATE) + INTERVAL 1 DAY + INTERVAL 2 HOUR)
DO
CALL batch_update_member_level();
```