preface

The user table in the previous database had unique indexes for the user name, mobile phone number, and email, so there was no need to worry about duplication. However, since both cell phone numbers and mailboxes can be null, and too much NULL can affect the stability of the index, remove the unique index and change the default value to an empty string. However, this raises the question of how to ensure that the mobile phone number (email) does not repeat in concurrent situations.

Causes of data duplication

When we need to insert or update fields that cannot be repeated, we do a query-insert (update) operation. However, because the operation is not atomic, it can result in the insertion of duplicate data in the case of concurrency.

Redis lock solution

Because of the atomic nature of the Redis command, we can try to use the Redis setnx command, such as setnx phone:13123456789 “”, if set successfully, we will get the lock of the phone number. Subsequent requests will simply fail because the lock cannot be obtained. Release the lock via del Phone :13123456789 after the request is processed.

As shown in the following code, the lock is obtained first. If the lock cannot be obtained, it is returned directly. If the lock is obtained, the service is processed. Finally, use the try-finally statement to release the lock to prevent the lock release failure.

        / / acquiring a lock
        if (Boolean.FALSE.equals(redisTemplate.opsForValue().setIfAbsent(redisKey, ""))) {
            return Result.fail(ErrorCodeEnum.OPERATION_CONFLICT, "Failed to acquire lock.");
        }

        try {
            // Business code
        } finally {
            / / releases the lock
            if (Boolean.FALSE.equals(redisTemplate.delete(redisKey))) {
                logger.error("Failed to release lock.")}}Copy the code

Encapsulated as a distributed lock service

Since the need for distributed locking is common, we encapsulate it as a service. The code is relatively simple, as shown below.

/** * Description: Distributed Lock service **@author xhsf
 * @create2020/12/10 nobleman * /
@Service
public class DistributedLockServiceImpl implements DistributedLockService {

    private final StringRedisTemplate redisTemplate;

    /** * lock key in Redis prefix */
    private static final String LOCK_KEY_REDIS_PREFIX = "distributed-lock:";

    /** * the value locked in Redis */
    private static final String LOCK_DEFAULT_VALUE_IN_REDIS = "";

    public DistributedLockServiceImpl(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /** * Obtain distributed locks, which are not automatically released **@errorCodeInvalidParameter: Incorrect key format * OperationConflict: Failed to obtain lock * *@paramKey Indicates the unique key * corresponding to the lock@returnGet the result */
    @Override
    public Result<Void> 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();
    }

    /** * Obtain a distributed lock, which is automatically released **@errorCodeInvalidParameter: Key or expirationTime format error * OperationConflict: Failed to obtain the lock * *@paramKey Indicates the unique key * corresponding to the lock@paramExpirationTime expirationTime of automatic lock release@paramTimeUnit timeUnit *@returnGet the result */
    @Override
    public Result<Void> 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();
    }

    /** * Release lock **@errorCodeInvalidParameter: key format error * InvalidParameter. NotExist: key * *@paramKey Indicates the unique key * corresponding to the lock@returnRelease the result */
    @Override
    public Result<Void> 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.");
        }
        returnResult.success(); }}Copy the code

Distributed lock service sample code

Here is an example of a service that registers an account with an SMS verification code.

    public Result<UserDTO> signUpBySmsAuthCode(String phone, String authCode, String password) {
        // Try to get a lock on the phone number
        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 {
            // Create user logic
        } finally {
            // Release the lock on the phone number
            if(! distributedLockService.releaseLock(phoneLockKey).isSuccess()) { logger.error("Failed to release phone lock. phoneLockKey={}", phoneLockKey); }}}Copy the code

Implement annotation locking using AOP

Locking code is always uncomfortable to add to business code, so we do it through annotations. A key for EL expressions is implemented here, which satisfies most of the requirements.

Add aspect annotations

Three arguments have been added to specify the key of the EL expression, the expiration time of the key lock, and the unit of time.

/** * Description: Distributed lock annotation **@author xhsf
 * @createThe 2020-12-10 then * /
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {

    /** * Distributed lock key that supports EL expressions such as #{#user.phone} */
    String value(a);

    /** * Expiration date */
    long expirationTime(a) default 0;

    /** * The unit of expiration time. The default value is seconds */
    TimeUnit timeUnit(a) default TimeUnit.SECONDS;

}
Copy the code

Implementation aspect

First, the key is constructed from the annotation and the parameters above the method, and then the lock is attempted. If the lock fails, the unified Result object is returned, and if the business logic is successfully executed. Finally, release the lock.

/** * Description: Distributed lock section, with {@linkDistributedLock} is a convenient way to use distributed locks@author xhsf
 * @create2020/12/10 as * /
@Aspect
public class DistributedLockAspect {

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

    @Reference
    private DistributedLockService distributedLockService;

    /** * EL expression parser */
    private static final ExpressionParser expressionParser = new SpelExpressionParser();

    /** * add a distributed lock to the method@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 {
        / / key
        String key = getKey(joinPoint, distributedLock);

        // Try to acquire the lock
        if(! getLock(key, distributedLock)) {return Result.fail(ErrorCodeEnum.OPERATION_CONFLICT, "Failed to acquire lock.");
        }

        // Execute the business logic
        try {
            return joinPoint.proceed();
        } finally {
            / / releases the lockreleaseLock(key, joinPoint); }}/**
     * 获取 key
     *
     * @param joinPoint ProceedingJoinPoint
     * @param distributedLock DistributedLock
     * @return key
     */
    private String getKey(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
        // Get the Map of the method parameters
        String[] parameterNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        Object[] parameterValues = joinPoint.getArgs();
        Map<String, Object> parameterMap = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            parameterMap.put(parameterNames[i], parameterValues[i]);
        }

        // Parse the EL expression
        String key = distributedLock.value();
        return getExpressionValue(key, parameterMap);
    }

    /** * Get lock **@paramThe key key *@param distributedLock DistributedLock
     * @returnGet the result */
    private boolean getLock(String key, DistributedLock distributedLock) {
        // Determine whether the timeout period needs to be set
        long expirationTime = distributedLock.expirationTime();
        if (expirationTime > 0) {
            TimeUnit timeUnit = distributedLock.timeUnit();
            return distributedLockService.getLock(key, expirationTime, timeUnit).isSuccess();
        }
        return distributedLockService.getLock(key).isSuccess();
    }

    /** * Release lock **@paramThe key 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())); }}/** * Get the value of the EL expression **@paramElExpression EL Expression *@paramParameterMap Parameter name - Value Map *@returnThe value of the expression */
    private String getExpressionValue(String elExpression, Map<String, Object> parameterMap) {
        Expression expression = expressionParser.parseExpression(elExpression, new TemplateParserContext());
        EvaluationContext context = new StandardEvaluationContext();
        for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
            context.setVariable(entry.getKey(), entry.getValue());
        }
        returnexpression.getValue(context, String.class); }}Copy the code

Note distributed lock use examples

Add the @distributedLock annotation and specify parameters to the following code.

    @DistributedLock("phone:#{#phone}")
    public Result<UserDTO> signUpBySmsAuthCode(String phone, String authCode, String password) {
    	// Business code
    }
Copy the code

Note that you need to register the aspect as a Bean

    /** * Distributed lock section **@return DistributedLockAspect
     */
    @Bean
    public DistributedLockAspect distributedLockAspect(a) {
        return new DistributedLockAspect();
    }
Copy the code

Using Redisson

Redisson has implemented a variety of distributed locks. You can use Redisson directly, which is much more powerful.