一、引起数据重复的原因:
引起数据库被重复插入的原因无外乎这几个,表象原因可能是网络延迟、用户连点、高并发等等,实际上是我们在数据库设计、代码逻辑的严谨性上出了问题。
网络延迟、用户连点:用户填写完表单,然后点击提交按钮,由于网络延迟,迟迟得不到后台的响应,这时用户会下意识的再次点击,这就导致了用户的连点,从而导致后端再第一次数据没处理完的时候,又进行了第二次提交,导致数据重复。
高并发场景:同个时间点,提交大量相同请求。
二、解决数据重复的方案(幂等性):
幂等:在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
1. 前端解决方案(不可靠,推荐)
前端解决办法就是:用户点击按钮后,让按钮点击失效或者禁用,待后端响应完成后,按钮恢复可用状态。
2. 数据库解决方案(可靠,推荐,适合高并发场景)
(1)在数据库中添加唯一索引或唯一约束:幂等主要手段就是通过表中的唯一约束实现,所以可以在需要去重的列上添加唯一索引,这样在插入数据时,重复数据会引发唯一索引冲突的错误,从而避免插入相同数据。
(2)在代码中使用集合去重:可以在代码中使用集合对要插入的数据进行去重操作,然后将去重后的数据插入数据库。例如,使用 Java 中的 HashSet,由于 HashSet 会自动去重,因此可以将数据插入 HashSet 中,再将去重后的数据插入数据库。
(3)在应用程序中进行去重(不推荐):在插入数据之前,可以查询数据库中是否已经存在相同数据。如果存在相同数据,可以选择更新已有数据或者直接跳过插入操作。(不推荐先查询再插入的方案,主要是因为性能效率不好,以及在高并发场景下并不能满足需求。)
(4)使用 ignore 关键字:如果是用主键 primary 或者唯一索引 unique 区分了记录的唯一性,避免插入重复记录可以使用 IGNORE 关键字,这样当有重复记录时就会忽略插入,执行后返回数字 0:IGNORE 只关注主键对应记录是不存在,无则添加,有则忽略。
INSERT IGNORE into user VALUES('1','joshua317','13299999999');
3. 悲观锁解决方案(可靠,推荐,适合高并发场景)
悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的;
特点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;
加解锁伪代码如下:
/***
1、 客户端 A 请求服务器设置 key 的值,如果设置成功就表示加锁成功
2、 客户端 B 也去请求服务器设置 key 的值,如果返回失败,那么就代表加锁失败
3、 客户端 A 执行代码完成,删除锁
4、 客户端 B 在等待一段时间后在去请求设置 key 的值,设置成功
5、 客户端 B 执行代码完成,删除锁
**/
$lockKey = 'lock_key';
$lockVaule = 'lock_vaule';
$isLock = $redisHandle->set ($lockKey, $lockVaule, ['nx', 'ex' => $ttl]);//nx 代表当 key 不存在时设置 ex 代表设置过期时间
if ($isLock){// 1. 获取锁成功
// 2. 处理业务
// 3. 解锁
$redisHandle->del($lockKey);
} else {
// 资源被其他请求占用,提示服务繁忙,请稍后再试
}
意外情况发生
- 假设锁提前过期后,客户端 A 还没执行完,然后客户端 B 获取到了锁,这时候客户端 A 执行完了,会不会在删锁的时候把 B 的锁给删掉?
$lockKey = 'lock_key';
$lockVaule = 'lock_key_'.uniqid (); // 分配一个随机值
$isLock = $redisHandle->set ($lockKey, $lockVaule, ['nx', 'ex' => $ttl]);//nx 代表当 key 不存在时设置 ex 代表设置过期时间
if ($isLock) {// 1. 获取锁成功
if ($redisHandle->get ($lockKey) == $value) { // 防止提前过期,误删其它请求创建的锁
// 2. 处理业务
// 3. 解锁
$redisHandle->del($lockKey);
} else {
// 资源被其他请求占用,提示服务繁忙,请稍后再试
}
} else {
// 资源被其他请求占用,提示服务繁忙,请稍后再试
}
2. 假设客户端业务处理中断,解锁失败导致锁没有释放且过期时间未到,然后客户端 B 却无法获取锁进行处理呢?
$lockKey = 'lock_key';
$lockVaule = 'lock_key_'.uniqid (); // 分配一个随机值
try {
$isLock = $redisHandle->set ($lockKey, $lockVaule, ['nx', 'ex' => $ttl]);//nx 代表当 key 不存在时设置 ex 代表设置过期时间
if ($isLock) {// 1. 获取锁成功
if ($redisHandle->get ($lockKey) == $value) { // 防止提前过期,误删其它请求创建的锁
// 2. 处理业务
// 3. 解锁
$redisHandle->del($lockKey);
} else {
// 资源被其他请求占用,提示服务繁忙,请稍后再试
}
} else {
// 资源被其他请求占用,提示服务繁忙,请稍后再试
}
} catch (Exception $e) {
// 3. 解锁
$redisHandle->del($lockKey);
}