SpringBoot分布锁--本地锁、Redis分布式锁

目录

 

1.什么是幂等

2.产生原因

3.解决方法

3.1前端js提交禁止,按钮可以用一些js组件

3.2使用Post/Redirect/Get模式

3.3本地锁(重点)

3.4Redis分布式锁

3.5请求事栗

1.什么是幂等

  •   select查询天然幂等
  • delete删除也是幂等,删除同一个多次效果一样

  • update直接更新某个值的,幂等

  • update更新累加操作的,非幂等

  • insert非幂等操作,每次新增一条

2.产生原因

由于重复点击或者网络重发 :

  • 点击提交按钮两次;
  • 点击刷新按钮;
  • 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
  • 使用浏览器历史记录重复提交表单;
  • 浏览器重复的HTTP请;
  • nginx重发等情况;
  • 分布式RPC的try重发等;

3.解决方法

3.1前端js提交禁止,按钮可以用一些js组件

3.2使用Post/Redirect/Get模式

   在提交后执行重定向

3.3本地锁(重点)

先获取参数接口信息

package com.benjaming_boot.resubmit.annoation;

import java.lang.annotation.*;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/8/27 10:20
 * @Description: 锁参数
 */
@Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
    //字段名称
    String name() default "";
}

接口自定义注解

package com.benjaming_boot.resubmit.annoation;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/8/26 20:53
 * @Description:
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {
    /**
     *description: 生成key的前缀
     *@Author: Zhubencai
     *@date: 2019/8/27 10:17
    */
    String prefix() default "";
    /**
     *description: 延时时间多久后再次提交
     *@Author: Zhubencai
     *@date: 2019/8/26 20:54
    */

    int expire() default 10;

    //默认时间单位是 秒
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     *description: key的分隔符,默认 :
     *@Author: Zhubencai
     *@date: 2019/8/27 10:18
    */
    String delimiter() default ":";
}

key的生成策略 

package com.benjaming_boot.resubmit;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/8/27 10:06
 * @Description: KEY生成器
 */
public interface CacheKeyGenerator {
    /**
     *description: 获取AOP参数,生成指定缓存KEY
     *@Author: Zhubencai
     *@date: 2019/8/27 10:07
    */
    String getLockKey(ProceedingJoinPoint point);
}

 

package com.benjaming_boot.resubmit;

import com.benjaming_boot.resubmit.annoation.CacheParam;
import com.benjaming_boot.resubmit.annoation.Resubmit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/8/27 10:08
 * @Description: 主要是解析 Resubmit 注解的属性,获取对应的属性值,生成一个全新的key
 */
@Service
public class LockKeyGenerator implements CacheKeyGenerator{
    @Override
    public String getLockKey(ProceedingJoinPoint point) {
        Method method = ((MethodSignature)point.getSignature()).getMethod();
        Resubmit resubmit = method.getAnnotation(Resubmit.class);
        Object[] args = point.getArgs();
        Parameter[] parameters = method.getParameters();
        StringBuilder builder = new StringBuilder();
        //todo 默认解析方法里面带 CacheParam 注解的属性,如果没有尝试着解析实体对象中的
        for(int i=0;i<parameters.length;i++){
            final CacheParam param = parameters[i].getAnnotation(CacheParam.class);
            if(param == null){
                continue;
            }
            System.out.println("args[i]:"+args[i]);
            builder.append(resubmit.delimiter()).append(args[i]);
        }
        if(StringUtils.isEmpty(builder.toString())){
            Annotation[][] annotations = method.getParameterAnnotations();
            System.out.println(annotations.length);
            for(int i = 0 ; i < annotations.length ; i++){
                final Object object = args[i];
                final Field[] fields = object.getClass().getDeclaredFields();
                for(Field field : fields){
                    System.out.println("当前字段为:"+field);
                    final CacheParam annotationParam = field.getAnnotation(CacheParam.class);
                    System.out.println(annotationParam.name());
                    if(annotationParam == null){
                        continue;
                    }
                    field.setAccessible(true);
                    builder.append(resubmit.delimiter()).append(ReflectionUtils.getField(field, object));
                }
            }
        }
        return resubmit.prefix() + builder.toString();
    }
}

 切面接口的实现

package com.benjaming_boot.resubmit;

import com.benjaming_boot.resubmit.annoation.Resubmit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.UUID;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/8/26 21:11
 * @Description: 数据重复提交校验
 */
@Aspect
@Component
public class ResubmitDataAspect {

    @Autowired
    private CacheKeyGenerator lockKeyGenerator;

    @Around("execution(public * *(..)) && @annotation(com.benjaming_boot.resubmit.annoation.Resubmit)")
    public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable{
        Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
        //todo 获取注解信息
        Resubmit resubmit = method.getAnnotation(Resubmit.class);
        if(StringUtils.isEmpty(resubmit.prefix())){
            throw new RuntimeException("lock key don't null ...");
        }
        final String lockKey = lockKeyGenerator.getLockKey(joinPoint);
        String value = UUID.randomUUID().toString();
       boolean lock = false;
       try{
           lock = ResubmitLock.getInstance().lock(lockKey, value);
           if(lock){
               return joinPoint.proceed();
           }else {
               throw new RuntimeException("重复提交");
           }
       }finally {
           ResubmitLock.getInstance().unlock(lock,lockKey , resubmit.expire());
       }
    }
}

3.4Redis分布式锁

package com.benjaming_boot.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/9/1 16:35
 * @Description:
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisLock {
    private static final String DELIMITER = "|";

    private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);

    private final StringRedisTemplate stringRedisTemplate;

    public RedisLock(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }
    /**
     *description: 获取锁 (存在死锁的风险)
     *@Author: Zhubencai
     *@date: 2019/9/1 16:48
     *@param: value value
     * @param time 超时时间
     * @param unit 过期时间单位
     *@return: true or false
    */
    public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit){
        return stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
            connection.set(lockKey.getBytes(), value.getBytes(),Expiration.from(time,unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
    }

    public boolean lock(String lockKey,final String uuid,long timeout,final TimeUnit unit){
        final long milliseconds = Expiration.from(timeout,unit).getExpirationTimeInMilliseconds();
        boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
        if(success){
            stringRedisTemplate.expire(lockKey,timeout,TimeUnit.SECONDS);
        }else{
            String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey,(System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
            final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
            if(Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()){
                return true;
            }
        }
        return success;
    }


    /**
     * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>
     */
    public void unlock(String lockKey, String value) {
        unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
    }

    /**
     * 延迟unlock
     *
     * @param lockKey   key
     * @param uuid      client(最好是唯一键的)
     * @param delayTime 延迟时间
     * @param unit      时间单位
     */
    public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
        if (StringUtils.isEmpty(lockKey)) {
            return;
        }
        if (delayTime <= 0) {
            doUnlock(lockKey, uuid);
        } else {
            EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
        }
    }

    /**
     * @param lockKey key
     * @param uuid    client(最好是唯一键的)
     */
    private void doUnlock(final String lockKey, final String uuid) {
        String val = stringRedisTemplate.opsForValue().get(lockKey);
        final String[] values = val.split(Pattern.quote(DELIMITER));
        if (values.length <= 0) {
            return;
        }
        if (uuid.equals(values[1])) {
            stringRedisTemplate.delete(lockKey);
        }
    }
}

aop切面逻辑

package com.benjaming_boot.resubmit;

import com.benjaming_boot.resubmit.annoation.Resubmit;
import com.benjaming_boot.util.RedisLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.UUID;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/9/1 16:57
 * @Description:
 */
@Aspect
@Configuration
public class ResubmitRedisAspect {

    @Autowired
    public ResubmitRedisAspect(RedisLock redisLock,CacheKeyGenerator cacheKeyGenerator){
        this.redisLock = redisLock;
        this.cacheKeyGenerator = cacheKeyGenerator;
    }
    private final RedisLock redisLock;
    private final CacheKeyGenerator cacheKeyGenerator;


    @Around("execution(public * *(..)) && @annotation(com.benjaming_boot.resubmit.annoation.Resubmit)")
    public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable{
        Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
        //todo 获取注解信息
        Resubmit resubmit = method.getAnnotation(Resubmit.class);
        if(StringUtils.isEmpty(resubmit.prefix())){
            throw new RuntimeException("lock key don't null ...");
        }
        final String lockKey = cacheKeyGenerator.getLockKey(joinPoint);
        String value = UUID.randomUUID().toString();
        boolean lock = false;
        try{
            lock = redisLock.lock(lockKey,value , resubmit.expire(),resubmit.timeUnit());
            if(lock){
                return joinPoint.proceed();
            }else {
                throw new RuntimeException("重复提交");
            }
        }finally {
            redisLock.unlock(lockKey,value);
        }
    }
}

3.5请求事栗

实体类

package com.benjaming_boot.controller;

import com.benjaming_boot.resubmit.annoation.CacheParam;

import java.io.Serializable;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/8/27 14:12
 * @Description:
 */

public class Coustomer implements Serializable {
    @CacheParam(name = "name")
    private String name;

    @CacheParam(name = "age")
    private String age;

    @CacheParam(name = "测试注解")
    public void say(){
        System.out.println("测试注解");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

Controller层

package com.benjaming_boot.controller;

import com.benjaming_boot.resubmit.annoation.CacheParam;
import com.benjaming_boot.resubmit.annoation.Resubmit;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: Zhubencai
 * @Date: create in 2019/8/27 12:54
 * @Description:
 */
@RestController
public class CoustomerController {

    @RequestMapping("/savePolicy")
    @Resubmit(prefix = "policy")
    public void savePolicy(@RequestBody Coustomer coustomer){
        System.out.println("保存成功");
    }

    @RequestMapping("/saveUser")
    @Resubmit(prefix = "user")
    public void saveUser(@CacheParam(name = "name") @RequestParam String name){
        System.out.println("保存成功");
    }
}

4.参考地址

https://mp.weixin.qq.com/s/9fidsb3RRciqFgCf7UKCQg

https://blog.battcn.com/2018/06/13/springboot/v2-cache-redislock/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值