前言

之前数据库的用户表的用户名、手机号码、邮箱都是设置了唯一索引,因此不需要考虑重复的问题。然而,由于手机号码和邮箱都可以为 null,而太多的 null 会影响索引的稳定性,因此去掉唯一索引并将默认值改为空字符串。但是这又引出了新的问题,如何保证在并发情况下手机号码(邮箱)不重复?

导致数据重复的原因

在需要插入或者更新不能重复的字段时,我们会进行 查询-插入(更新) 的操作。然而,由于该操作并不是原子的,因此在并发的情况下可能导致插入重复的数据。

Redis 锁解决方案

由于 Redis 命令的原子特性,我们可以尝试使用 Redis 的 setnx 命令,比如 setnx phone:13123456789 '',若设置成功,则拿到了该手机号码的锁。后续请求会因为无法拿到该锁而直接失败。在请求处理结束后再通过 del phone:13123456789 释放该锁。

如下代码所示,先获取锁,若获取不到直接返回,若获取到则进行业务处理。最后使用 try-finally 语句释放锁,防止锁释放失败。

// 获取锁

if (Boolean.FALSE.equals(redisTemplate.opsForValue().setIfAbsent(redisKey, ""))) {

return Result.fail(ErrorCodeEnum.OPERATION_CONFLICT, "Failed to acquire lock.");

}

try {

// 业务代码

} finally {

// 释放锁

if (Boolean.FALSE.equals(redisTemplate.delete(redisKey))) {

logger.error("Failed to release lock.")

}

}

复制代码

封装成分布式锁服务

由于分布式锁的需求很常见,因此我们封装成服务。代码比较简单,如下所示。

/**

* 描述:分布式锁服务

*

* @author xhsf

* @create 2020/12/10 19:13

*/

@Service

public class DistributedLockServiceImpl implements DistributedLockService{

private final StringRedisTemplate redisTemplate;

/**

* 锁的 key 在 Redis 里的前缀

*/

private static final String LOCK_KEY_REDIS_PREFIX = "distributed-lock:";

/**

* 锁在 Redis 里的值

*/

private static final String LOCK_DEFAULT_VALUE_IN_REDIS = "";

public DistributedLockServiceImpl(StringRedisTemplate redisTemplate){

this.redisTemplate = redisTemplate;

}

/**

* 获取分布式锁,不会自动释放锁

*

* @errorCode InvalidParameter: key 格式错误

* OperationConflict: 获取锁失败

*

* @param key 锁对应的唯一 key

* @return 获取结果

*/

@Override

public Result getLock(String key){

String redisKey = LOCK_KEY_REDIS_PREFIX + key;

if (Boolean.FALSE.equals(redisTemplate.opsForValue().setIfAbsent(redisKey, LOCK_DEFAULT_VALUE_IN_REDIS))) {

return Result.fail(ErrorCodeEnum.OPERATION_CONFLICT, "Failed to acquire lock.");

}

return Result.success();

}

/**

* 获取分布式锁,锁到期自动释放

*

* @errorCode InvalidParameter: key 或 expirationTime 格式错误

* OperationConflict: 获取锁失败

*

* @param key 锁对应的唯一 key

* @param expirationTime 锁自动释放时间

* @param timeUnit 时间单位

* @return 获取结果

*/

@Override

public Result getLock(String key, Long expirationTime, TimeUnit timeUnit){

String redisKey = LOCK_KEY_REDIS_PREFIX + key;

if (Boolean.FALSE.equals(redisTemplate.opsForValue().setIfAbsent(

redisKey, LOCK_DEFAULT_VALUE_IN_REDIS, expirationTime, timeUnit))) {

return Result.fail(ErrorCodeEnum.OPERATION_CONFLICT, "Failed to acquire lock.");

}

return Result.success();

}

/**

* 释放锁

*

* @errorCode InvalidParameter: key 格式错误

* InvalidParameter.NotExist: key 不存在

*

* @param key 锁对应的唯一 key

* @return 释放结果

*/

@Override

public Result releaseLock(String key){

String redisKey = LOCK_KEY_REDIS_PREFIX + key;

if (Boolean.FALSE.equals(redisTemplate.delete(redisKey))) {

return Result.fail(ErrorCodeEnum.INVALID_PARAMETER_NOT_EXIST, "The lock does not exist.");

}

return Result.success();

}

}

复制代码

分布式锁服务示例代码

这里是一个通过短信验证码注册账号的服务示例。

public Result signUpBySmsAuthCode(String phone, String authCode, String password){

// 尝试获取关于该手机号码的锁

String phoneLockKey = PHONE_DISTRIBUTED_LOCK_KEY_PREFIX + phone;

if (!distributedLockService.getLock(phoneLockKey).isSuccess()) {

return Result.fail(ErrorCodeEnum.OPERATION_CONFLICT, "Failed to acquire phone lock.");

}

try {

// 创建用户的逻辑

} finally {

// 释放锁关于该手机号码的锁

if (!distributedLockService.releaseLock(phoneLockKey).isSuccess()) {

logger.error("Failed to release phone lock. phoneLockKey={}", phoneLockKey);

}

}

}

复制代码

使用 AOP 实现注解加锁

加锁代码添加到业务代码里,总让人感觉不舒服,因此我们通过注解的方式进行加锁。这里实现了 EL 表达式的 key,可以满足大部分需求。

添加切面注解

这里添加了3个参数,可以指定 EL 表达式的 key,key 锁的过期时间和时间单位。

/**

* 描述: 分布式锁注解

*

* @author xhsf

* @create 2020-12-10 21:16

*/

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface DistributedLock {

/**

* 分布式锁 key,支持 EL 表达式,如#{#user.phone}

*/

String value();

/**

* 过期时间

*/

long expirationTime() default 0;

/**

* 过期时间单位,默认为秒

*/

TimeUnit timeUnit() default TimeUnit.SECONDS;

}

复制代码

实现切面

先通过注解和方法上面的参数构造 key,然后尝试加锁,若加锁失败返回统一的 Result 对象,若成功执行业务逻辑。最后释放锁。

/**

* 描述:分布式锁切面,配合 {@link DistributedLock} 可以便捷的使用分布式锁

*

* @author xhsf

* @create 2020/12/10 21:10

*/

@Aspect

public class DistributedLockAspect{

private static final Logger logger = LoggerFactory.getLogger(DistributedLockAspect.class);

@Reference

private DistributedLockService distributedLockService;

/**

* EL 表达式解析器

*/

private static final ExpressionParser expressionParser = new SpelExpressionParser();

/**

* 给方法添加分布式锁

*

* @param joinPoint ProceedingJoinPoint

* @return Object

*/

@Around("@annotation(com.xiaohuashifu.recruit.external.api.aspect.annotation.DistributedLock) " +

"&& @annotation(distributedLock)")

public Object handler(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable{

// 获得键

String key = getKey(joinPoint, distributedLock);

// 尝试获取锁

if (!getLock(key, distributedLock)) {

return Result.fail(ErrorCodeEnum.OPERATION_CONFLICT, "Failed to acquire lock.");

}

// 执行业务逻辑

try {

return joinPoint.proceed();

} finally {

// 释放锁

releaseLock(key, joinPoint);

}

}

/**

* 获取 key

*

* @param joinPoint ProceedingJoinPoint

* @param distributedLock DistributedLock

* @return key

*/

private String getKey(ProceedingJoinPoint joinPoint, DistributedLock distributedLock){

// 获得方法参数的 Map

String[] parameterNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();

Object[] parameterValues = joinPoint.getArgs();

Map parameterMap = new HashMap<>();

for (int i = 0; i < parameterNames.length; i++) {

parameterMap.put(parameterNames[i], parameterValues[i]);

}

// 解析 EL 表达式

String key = distributedLock.value();

return getExpressionValue(key, parameterMap);

}

/**

* 获取锁

*

* @param key 键

* @param distributedLock DistributedLock

* @return 获取结果

*/

private boolean getLock(String key, DistributedLock distributedLock){

// 判断是否需要设置超时时间

long expirationTime = distributedLock.expirationTime();

if (expirationTime > 0) {

TimeUnit timeUnit = distributedLock.timeUnit();

return distributedLockService.getLock(key, expirationTime, timeUnit).isSuccess();

}

return distributedLockService.getLock(key).isSuccess();

}

/**

* 释放锁

*

* @param key 键

* @param joinPoint ProceedingJoinPoint

*/

private void releaseLock(String key, ProceedingJoinPoint joinPoint){

if (!distributedLockService.releaseLock(key).isSuccess()) {

logger.error("Failed to release lock. key={}, signature={}, parameters={}",

key, joinPoint.getSignature(), Arrays.toString(joinPoint.getArgs()));

}

}

/**

* 获取 EL 表达式的值

*

* @param elExpression EL 表达式

* @param parameterMap 参数名-值 Map

* @return 表达式的值

*/

private String getExpressionValue(String elExpression, Map parameterMap){

Expression expression = expressionParser.parseExpression(elExpression, new TemplateParserContext());

EvaluationContext context = new StandardEvaluationContext();

for (Map.Entry entry : parameterMap.entrySet()) {

context.setVariable(entry.getKey(), entry.getValue());

}

return expression.getValue(context, String.class);

}

}

复制代码

注解分布式锁使用示例

如下代码,添加 @DistributedLock 注解并指定参数即可。

@DistributedLock("phone:#{#phone}")

public Result signUpBySmsAuthCode(String phone, String authCode, String password){

// 业务代码

}

复制代码

注意,需要注册切面为 Bean

/**

* 分布式锁切面

*

* @return DistributedLockAspect

*/

@Bean

public DistributedLockAspect distributedLockAspect(){

return new DistributedLockAspect();

}

复制代码

使用 Redisson

看了 whosYourDaddy 的评论才知道 Redisson 已经实现了各种分布式锁,大家可以直接使用 Redisson,功能更加强大。

mysql 索引不重复的值,【锁】在数据库无法使用唯一索引时如何保证数据的不重复?...相关推荐

  1. mysql 查询数据库索引语句_利用SQL语句查询数据库中所有索引

    本章我们就要讲解一下如何利用sql语句来查询出数据库中所有索引明细.当然了,我们可以在microsoft sql server management studio中选择"表"- & ...

  2. mysql查询为0的值_MySql查询整型字段空字符时出现为0的数据

    表结构: -- 表的ddl CREATE TABLE `user_desc` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `USER_NAME` varchar(2 ...

  3. 数据库,唯一索引,重复数据处理

    为什么80%的码农都做不了架构师?>>>    //唯一索引,重复数据处理 1.为母表建立唯一主键(主键为自增,此字段在后面删除记录时会用到),同时 建立和母表一样的临时表(此表要建 ...

  4. sql加上唯一索引后批量插入_MySQL批量插入遇上唯一索引避免方法

    一.背景 以前使用SQL Server进行表分区的时候就碰到很多关于唯一索引的问题:Step8:SQL Server 当表分区遇上唯一约束,没想到在MySQL的分区中一样会遇到这样的问题:MySQL表 ...

  5. rocketmq怎么保证数据不会重复_rocketmq如何保证消息不丢失

    一.大体可以从三方面来说: 分别从Producer发送机制.Broker的持久化机制,以及消费者的offSet机制来最大程度保证消息不易丢失 从Producer的视角来看:如果消息未能正确的存储在MQ ...

  6. linux下mysql写中文变成问号_如何解决数据库插入中文字体时显示问号

    欢迎点击「算法与编程之美」关注我们! 本文首发于微信公众号:"算法与编程之美",欢迎关注,及时了解更多此系列文章. 问题描述 我们在进行数据库的增删改查的操作时,当我们插入英文或者 ...

  7. rocketmq怎么保证数据不会重复_RocketMQ保证信息有序性和防止重复

    分布式开放消息系统(RocketMQ)的原理与实践 分布式消息系统做为实现分布式系统可扩展.可伸缩性的关键组件,须要具备高吞吐量.高可用等特色.而谈到消息系统的设计,就回避不了两个问题:java 消息 ...

  8. mysql返回惟一不同值_SQL/MySQL-选择不同/唯一但返回所有列?

    jeck猫 你要找的是:select *from tablegroup by field1有时可以用不同的on语句来写:select distinct on field1 *from table然而, ...

  9. rocketmq怎么保证数据不会重复_阿里架构师亲授:Kafka和RocketMQ的消息复制实现的差异点在哪?...

    众所周知,消息队列在收发两端,主要是依靠业务代码,配合请求确认的机制,来保证消息不会丢失的.而在服务端,一般采用持久化和复制的方式来保证不丢消息. 把消息复制到多个节点上,不仅可以解决丢消息的问题,还 ...

最新文章

  1. 企业可视化报表工具选型经验分享
  2. 《iOS 8应用开发入门经典(第6版)》——第1章,第1.6节小结
  3. 什么叫点积的巧记理解
  4. Tensorflow快餐教程(8) - 深度学习简史
  5. IsWow64的真实用途
  6. Cookie/Session机制详解--如何区分不同用户
  7. DHCP中继以及DHCP数据库的备份和还原
  8. WPF Application启动界面设置——
  9. 填充一个池需要多少个线程?
  10. JAVA开发工具下载
  11. python海龟绘图圆形_python之海龟绘图
  12. url 解析一个url里面的参数,获取地址栏参数信息
  13. 基于深度强化学习的离散自动生产线智能调度
  14. Hi3512的IPCAM开发
  15. 计算机运行内存和显卡内存不足,Win10系统提示计算机显卡内存不足该怎么办?...
  16. 这位智商奇高的超级天才去了:他简直活出了别人八辈子的精彩!
  17. 饱和蒸汽比容计算、 温压补偿系数计算
  18. 使用Android的Service实现后台定时检测并重启应用
  19. 骑行318、 2016.7.26
  20. 路由器卫士有android-,路由器卫士APP全面介绍

热门文章

  1. 测试分析人员必备知识—需求管理和可追溯性矩阵
  2. 一个TCP/IP转发的例子
  3. 2450 Problem B 树的高度
  4. ASPxPivotGrid CollapseAll 方法为什么不起作用
  5. 固定翼飞机的一些记录——(1)IMU
  6. mysql shutdown_紧急请教: mysql 无法正常启动 /usr/sbin/mysqld: Shutdown complete
  7. 点石互动--highdiy之:Google补充结果探讨
  8. FPGA CRC-16/XMODEM x16+x12+x5+1
  9. 我是刘洪蕾,我爱java,我也爱编程
  10. Linux/macOS的打包、压缩、解压缩