失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > mysql 索引不重复的值 【锁】在数据库无法使用唯一索引时如何保证数据的不重复?...

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

时间:2024-02-07 14:52:26

相关推荐

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

前言

之前数据库的用户表的用户名、手机号码、邮箱都是设置了唯一索引,因此不需要考虑重复的问题。然而,由于手机号码和邮箱都可以为 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 /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 -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 /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 索引不重复的值 【锁】在数据库无法使用唯一索引时如何保证数据的不重复?...》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。