封面《Summer Pockets》
前言
在涉及数据库主键的时候,我们有 mysql 自增 id、UUID、雪花 id 等。自增 id 在小项目中好用,但是自增 id 在分布式微服务中,因为水平分库分表,存在 id 重复问题,同时存在着高并发抢锁的性能问题。如果使用 UUID 则主键不自增,容易发生页裂问题且不方便分库分表和范围查询。使用雪花 id 能够很好的解决上述问题,但是雪花 id 会发生时钟回拨问题和存储消耗资源多。
因此可以用业务上自定义时间 + Sequence 来实现自增主键,但是 mysql 像 oracle 一样支持 sequence,因此需要使用一张 table 来模拟。
创建 Sequence 表
先创建一张 sequence
表,其中存放了 sequence 的名称、当前值、步长、最小值和最大值。DDL 语句如下
1 2 3 4 5 6 7 8 CREATE TABLE sequence ( seq_name VARCHAR (50 ) NOT NULL COMMENT 'seq名' , current_value BIGINT NOT NULL COMMENT '当前值' , step_value BIGINT NOT NULL DEFAULT 1 COMMENT '步长' , min_value BIGINT NOT NULL COMMENT '最小值' , max_value BIGINT NOT NULL COMMENT '最大值' , PRIMARY KEY (seq_name) );
为了方便后续测试,在这里先插入一条 sequence 值
1 INSERT INTO `sequence` (`seq_name`, `current_value`, `step_value`, `min_value`, `max_value`) VALUES ('test' , '0' , '10' , '0' , '999' );
nextval 函数
可以在业务层实现 sequence,也可以在 DB 层面实现。在这里采用 DB 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 DROP FUNCTION IF EXISTS `nextval`;DELIMITER / / CREATE FUNCTION `nextval`(seq_name VARCHAR (50 )) RETURNS BIGINT DETERMINISTIC MODIFIES SQL DATABEGIN DECLARE current_val BIGINT ; DECLARE step_val BIGINT ; DECLARE min_val BIGINT ; DECLARE max_val BIGINT ; DECLARE next_val BIGINT ; SELECT current_value, step_value, min_value, max_value INTO current_val, step_val, min_val, max_val FROM sequence WHERE seq_name = seq_name; SET next_val = (current_val + step_val - min_val) % (max_val - min_val + 1 ) + min_val; UPDATE sequence SET current_value = next_val WHERE seq_name = seq_name; RETURN next_val; END / / DELIMITER ;
测试一下
悲观锁
在上面的方案中,因为没有加锁,在高并发的情况下可能会返回相同的 sequence 值,因此可以加悲观锁,在高并发的情况下保证不出现重复值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 DROP FUNCTION IF EXISTS `nextval`;DELIMITER / / CREATE FUNCTION `nextval`(seq_name VARCHAR (50 )) RETURNS BIGINT DETERMINISTIC MODIFIES SQL DATABEGIN DECLARE current_val BIGINT ; DECLARE step_val BIGINT ; DECLARE min_val BIGINT ; DECLARE max_val BIGINT ; DECLARE next_val BIGINT ; SELECT current_value, step_value, min_value, max_value INTO current_val, step_val, min_val, max_val FROM sequence WHERE seq_name = seq_name FOR UPDATE ; SET next_val = (current_val + step_val - min_val) % (max_val - min_val + 1 ) + min_val; UPDATE sequence SET current_value = next_val WHERE seq_name = seq_name; RETURN next_val; END / / DELIMITER ;
乐观锁
虽然悲观锁可以保证不出重复值,但是在高并发的情况下,锁会导致性能下降,因此可以使用乐观锁来实现。在下面的代码里面使用了 try_times
来控制重试次数,最多重试 3 次,但是没有获取到 sequence 值则返回 0,需要在应用层进行判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 DROP FUNCTION IF EXISTS `nextval`;DELIMITER / / CREATE FUNCTION `nextval`(seq_name VARCHAR (50 )) RETURNS BIGINT DETERMINISTIC MODIFIES SQL DATABEGIN DECLARE current_val BIGINT ; DECLARE step_val BIGINT ; DECLARE min_val BIGINT ; DECLARE max_val BIGINT ; DECLARE next_val BIGINT ; DECLARE try_times INT ; SET try_times = 0 ; while try_times < 3 DO SELECT current_value, step_value, min_value, max_value INTO current_val, step_val, min_val, max_val FROM sequence WHERE seq_name = seq_name; SET next_val = (current_val + step_val - min_val) % (max_val - min_val + 1 ) + min_val; UPDATE sequence SET current_value = next_val WHERE seq_name = seq_name AND current_value = current_val; IF ROW_COUNT() > 0 THEN RETURN next_val; END IF; SET try_times = try_times + 1 ; END WHILE; RETURN 0 ; END / / DELIMITER ;
后记
除开在 sql 中实现函数以外,还可以把上述 sql 拆开放到业务层实现,同时加上分段和缓存来减少高并发情况下的数据库压力,这部分后续有空可以更新。
参考文献