文章目录
1. 需求背景
在实际的业务开发过程中,很多时候都需要保证数据的唯一性。比如我们在做的SAAS系统,租户有对应的配置数据,一个租户只能有一条数据。可以通过以下两个步骤来判断:
- 先查询这个租户是否存在这条配置数据?
- 如果没有则插入,有则忽略。
在并发低的场景是没有问题的,但是在高并发的场景下,两线程同时查到该条数据不存在,然后接着执行写入的逻辑,这样就导致了数据重复。
2. 解决方案
2.1 分布式锁
使用Redis分布式锁,所有的请求都串行化执行了。具体实现方式有几种:
- setNX代码方式
- Redission
- lock4j
使用这种方式要注意锁的粒度不能太大,
2.2 数据库实现
2.2.1 唯一索引
给表建立相应的唯一索引,比如:uk_tenant_id
。这样可以保证一个租户只有一条数据。适用于不需要逻辑删除的业务。
2.2.2 insert ignore写入
使用insert ... ignore
,mysql发现如果数据重复就忽略,否则就会插入。要求表中存在唯一索引或PRIMARY KEY。也不适用于逻辑删除的业务。
PS:使用insert ... ignore
也有可能会导致死锁。
2.2.3 insert on duplicate key update写入
使用insert on duplicate key update
,数据不存在则直接insert。如果发现数据已经存在了,则update。不过要求表中存在唯一索引或PRIMARY KEY。
PS:insert on duplicate key update
在高并发的情况下,可能会产生死锁问题
2.2.4 增加一张防重表(业务)
CREATE TABLE `config_unique` (
`id` bigint(20) NOT NULL COMMENT 'id',
`config_name` varchar(130) DEFAULT NULL COMMENT '名称',
`config_key` varchar(255) NOT NULL COMMENT '键',
`tenant_id` bigint(20) unsigned NOT NULL COMMENT '租户id',
`user_name` varchar(30) NOT NULL COMMENT '创建用户名称',
`create_date` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configName_configKey` (`config_name`,`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配置防重表';
注意表里并没有逻辑删除字段deleted。在添加配置数据之前,先添加防重表。如果添加成功,则说明可以正常添加配置,如果添加失败,则说明有重复数据。保证防重表和添加配置的操作必须要在同一个事务中,否则会出问题。
在代码中:
try {
transactionTemplate.execute((status) -> {
configUniqueMapper.insert(configUnique);
configMapper.insert(config);
return Boolean.TRUE;
});
} catch(DuplicateKeyException e) {
config = configMapper.query(config);
}
捕获到DuplicateKeyException 异常则说明已经插入过数据。
3. 总结
根据业务场景可以选择对应的方案进行数据防重处理,没有最好,只有合适的方案。