一、业务背景
通常业务系统的一些记录表都会有一些唯一性约束,例如相同用户下不允许重名;通常可以对指定列创建唯一性索引即可,例如:
CREATE TABLE `novel`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`novel_id` bigint(20) DEFAULT NULL,
`user_id` varchar(32) DEFAULT NULL,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `novel_name_unique_idx` (`user_id`, `name`) COMMENT '名字唯一性索引'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 ;
可以使用数据库唯一性约束保证不重复。但是线上系统通常对删除操作都是逻辑删除,并非物理删除,逻辑删除会有一个deleted字段标记是否删除,例如:
CREATE TABLE `novel`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`novel_id` bigint(20) DEFAULT NULL,
`deleted` int(11) DEFAULT '0',
`user_id` varchar(32) DEFAULT NULL,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `novel_name_unique_idx` (`user_id`, `deleted`, `name`) COMMENT '名字唯一性索引'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 ;
这种场景就会存在问题:相同名字的Novel只允许删除一次,删除两次会导致唯一性约束校验失败;如果不适用数据库索引而使用逻辑代码校验(一个账号允许多个用户同时登陆进行创建Novel),例如:
public void save(Novel novel){
//1: 业务逻辑条件校验
//2: 名字重复检查
if(novel.getName not in database ){
//3: save novel
}else{
return "名字冲突";
}
}
由于并发问题会导致两个线程同时在执行if novel.getName not in database 校验都通过而重复。为了解决这个问题供选方案如下:
二、拓展的数据库唯一性索引(推荐方案)
CREATE TABLE `novel`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`novel_id` bigint(20) DEFAULT NULL,
`deleted` int(11) DEFAULT '0',
`user_id` varchar(32) DEFAULT NULL,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`deleted_version` bigint(20) DEFAULT '0' COMMENT '删除版本号,删除时候值为ID,新建为0',
PRIMARY KEY (`id`),
UNIQUE KEY `novel_name_unique_idx` (`user_id`, `deleted`, `name`, `deleted_version`) COMMENT '名字唯一性索引'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 ;
由于之前的唯一性索引只有user_id,deleted,name 二次删除时候会冲突,因此可以再加一个字段解决此问题,也就是deleted_version字段;该字段用户首次创建时候值默认为0,当用户删除时候SQL如下
:
update novel set deleted_version = id , deleted = true where novel_id = 1 and user_id = 'zhangsan'
可以解决二次删除的冲突问题。