目录
业务背景
通常业务系统的一些记录表都会有一些唯一性约束,例如相同用户下不允许重名;通常可以对指定列创建唯一性索引即可,例如:
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 校验都通过而重复。为了解决这个问题供选方案如下:
分布式锁(创建串行化)
创建时候按照User_id获取分布式锁,每次只有一个登陆的用户允许创建,没有获取的分布式锁的进行等待。
弊端:对于高并发业务QPS上不去,影响并发。
分布式锁(对User_id与name加锁)
对于创建Novel时候事先对该用户预留该名字,相同用户再次预留该名字时候就会失败。
弊端:首次预留名字的创建失败,导致第二次预约的登陆用户也不可用该名字,体验不好。
拓展的数据库唯一性索引(推荐方案)
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'
就可以解决二次删除的冲突问题。