增删改 - 03 - 入参及操作校验

1. 常见入参校验

1.1 非空校验

栗子:
校验某些必填字段值,直接使用注解实现
@NotNull
@NotBlank
@NotEmpty

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.NotEmpty;

1.2 字符串校验

栗子:
比如姓名要求不超过10个中文字符,String 表示的日期必须符合某种格式,更多用于数值型的校验,下面给出一个简单的小栗子

public class ValidationUtils {
    /** 数字校验 */
    public static final String NUM_REGEX = "^\\d+$";

    /** 电话号码校验 */
    public static final String PHONE_REGEX = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$";

    /**  身份证号码校验 */
    public static final String ID_CARD_REGEX = "^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";

    /** 中文校验 */
    public static final String ZH_REGEX = "[\\u4e00-\\u9fa5]+";

    /** 中文校验,限制1-30个字符 */
    public static final String ZH_REGEX_LIMIT_30 = "^[\\u4e00-\\u9fa5]{1,30}$";

    /**
     *  英文、数字、下划线构成
     *  首字母只能为英文,末尾不能是下划线
     */
    public static final String ROLE_CODE_REGEX = "^[a-z][a-z0-9_]*[a-z]$";

    /** 英文、数字组成,不超过 30 个字符 */
    public static final String NUM_ENG_LIMIT_30 = "^[A-Za-z0-9]{1,30}$";

    /** 日期校验 - yyyy-mm-dd */
    public static final String DATE_REGEX = "^\\d{4}-\\d{2}-\\d{2}";

    /** 日期校验 - yyyy-mm */
    public static final String DATE_MONTH_REGEX = "^\\d{4}-\\d{2}";

    /**
     * 两位小数限制 - 整数位无限制
     * 0.xx 两位小数
     * xxxx.xx 两位小数
     */
    public static final String NUMBER_D2_REGEX = "^(0\\.\\d{1,2}|[1-9]\\d*(\\.\\d{1,2})?)$";

    /**
     * 三位小数限制 - 整数位最高 8 位限制
     * 0.xx 两位小数
     * xxxx.xxx 三位小数
     */
    public static final String NUMBER_D3_REGEX = "^(0\\.\\d{1,2}|[1-9]{1,8}\\d*(\\.\\d{1,3})?)$";

    /**
     * 一位小数限制,整数位最高 2 位 - 最高 99.9
     */
    public static final String NUMBER_D1_REGEX = "^(([1-9]?\\d(\\.\\d{1})?)|99.9)$";

    /** 小数位数无限制 */
    public static final String NUMBER_D_REGEX = "^(0\\.\\d{2}|[1-9]\\d*(\\.\\d+)?)$";

    /** 1-99的正整数 hundred */
    public static final String NUMBER_BETWEEN_ZERO_HUNDRED_REGEX = "^[1-9]\\d?$";

    /** > 0 的正整数 - 最高八位 */
    public static final String NUMBER_LIMIT_EIGHT_REGEX = "^[1-9][0-9]{1,8}$";

    /**
     * 判断字符串是否符合正则表达式
     *
     * @param str   输入的 string
     * @param regex 想要匹配的正则表达式
     * @return true - 表示匹配
     * false - 表示不匹配
     */
    public static boolean matchTargetRegex(String str, String regex) {
        boolean isMatch = Pattern.matches(regex, str);
        return isMatch;
    }

    /**
     * 判断一个 str 是否符合正则表达式且满足长度限制
     *
     * @param str
     * @param regex
     * @param limit
     * @return true - 满足表达式且满足长度
     */
    public static boolean matchTargetRegex(String str, String regex, int limit) {
        if (!matchTargetRegex(str, regex)) {
            return false;
        }
        return str.length() <= limit;
    }

    /**
     * 返回所有匹配正则表达式的子串字符长度和
     *
     * @param str   输入的 string
     * @param regex 想要匹配的正则表达式
     * @return -1 说明没有符合的子串
     * else 符合正则表达式的所有子串长度和
     */
    public static int subMatchCharCount(String str, String regex) {
        int count = 0;
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(str);
        while (m.find()) {
            count += (m.end() - m.start());
        }

        return count == 0 ? -1 : count;
    }

    /**
     * bigDecimal 数据 < 0 的时候,返回 zero
     *
     * @param num 传入的数据
     * @return
     */
    public static BigDecimal minZeroBdDeal(BigDecimal num) {
        if (num.compareTo(BigDecimal.ZERO) < 0) {
            num = BigDecimal.ZERO;
        }

        return num;
    }

    /**
     * 检查一个数字(Number类型)是否与给定的正则表达式匹配
     * @param num
     * @param regex
     * @return
     */
    public static boolean numMatchRegex(Number num, String regex) {

        return num == null || num.toString().matches(regex);
    }

    /**
     * 传入的 bigDecimal 数据是否在当前设置的最高位数内
     *
     * @param num       传入的 bigDecimal 数据
     * @param highLimit 小数点前-整数位的最高位数
     * @return true - 在最高范围内
     * false - 超出最高位数
     */
    public static boolean isInBigDecimalBeforeDot(BigDecimal num, int highLimit) {
        int beforeDot = num.precision() - num.scale();
        return beforeDot <= highLimit;
    }


}

1.3 唯一性校验

场景: 新增修改时保证某些字段的值唯一:

直接查询数据库就好,就像下面这样,and id != ? 是可选项,新增不用添加,修改时添加

select count(*) from table where column =and id != ?

对查询到的结果 res (sql语句的结果) 进行判断,true 说明唯一,false 说明不唯一

return res == 0 ?

2. 常见操作校验

2.1 接口防抖

就是防止请求在短时间内多次发送到同一个接口。

方案: 设置一个防抖时间,操作前在 redis 里面生成一个 key,设置过期时间 = 防抖时间,操作时判断有没有 key

有 key 就提示操作过于频繁,没有就创建key

如果想判断相同请求
方案 1: 找到传输数据中的特定标识数据,赋值 k - v
方案 2: 整个入参赋值为 v,每次判断 redis里的 k - v 的 value 是否和这次传输的 value 相同,相同&&在防抖时间内,不做更新,其余情况都更新

(但是不推荐第二种方案,redis 里不适合存储大数据的 value,redis 单线程读取 bigValue 和 bigKey 时,读取速度慢,再加上单线程处理机制,上一次没查完,第二次就要等待查询,容易造成阻塞现象)

举个栗子:优化之前流程里的 Controller 层写法

@RestController
@RequestMapping("/work-operator")
public class WorkOperatorController {

    @Autowired
    private IWorkOperatorService workOperatorService;

    @Resource
    private RedisUtil redisUtil;

    /** 设置防抖时间,1s */
    private final long TIME_LIMIT = 1L;

/**
     * 单笔数据新增
     * @param saveDTO
     * @return
     */
    @PostMapping("/create")
    public R<Object> create(@Valid @RequestBody WorkOperatorDTO saveDTO, @User UserBO userBO){
        //  判断入参是否正确
        if (checkDTO(saveDTO) != null) {
            return checkDTO(saveDTO);
        }
        //  入参正确后,可以进入添加方法,此时添加 key: 用户id + 方法名, value: 当前时间戳,防止接口抖动
        String key = userBO.getId() + "::" + "createWorkOperator";
        //  setKeyIfAbsent() : key不存在,设置成功,返回true; key存在,则设置失败,返回false
        Boolean lock =  redisUtil.tryLock(key, TIME_LIMIT, TimeUnit.SECONDS);
        if(lock == null || !lock){
            return R.fail(ConstantMessage.REPEAT_REQUEST);
        }

        return workOperatorService.createWorkOperator(saveDTO) ?
                R.ok(ConstantMessage.CREATE_SUCCESS) : R.fail(ConstantMessage.CREATE_FAIL);
    }

	/** 删除防抖,同理 */
    @PostMapping("/delete-batch")
    public R<Object> deleteBatch(@RequestBody List<Long> ids){

String key = userBO.getId() + "::" + "delWorkOperator";
        Boolean lock =  redisUtil.tryLock(key, TIME_LIMIT, TimeUnit.SECONDS);
        if(lock == null || !lock){
            return R.fail(ConstantMessage.REPEAT_REQUEST);
        }

        return  workOperatorService.deleteBatch(ids) ?
                R.ok(ConstantMessage.DELETE_SUCCESS) : R.fail(ConstantMessage.DELETE_FAIL + ConstantMessage.WORK_OPERATOR_UNBIND);
    }

}

为了防抖能够成功实现,不可以在操作完成后删除 redis 中的 key 值!
文章末尾给出 redisUtil


2.2 防止同一数据并发提交

场景: 有些重要的数据,我们希望许多有修改权限的人,同意时间内只有一个人修改该数据,简单来说就是给接口在并发情况下加锁。

方案: 使用 redis 来模拟锁,操作前加锁,操作结束释放锁
( 这类结束后释放资源型的代码,都可以使用 try-catch-finally 来处理,让系统自动释放资源 )

举个栗子:

public R<String> submit(@Validated @RequestBody AssessProblemSubmitRO submitVO) {
		//	进行校验
		//	开始防止并发现象
        Boolean lock = redisUtil.tryLock("submit_problem_" + taskDetailId, 3L, TimeUnit.MINUTES);
        Long count = 0L;
        // 未获取到缓存锁,自循环获取缓存锁,且自循环次数在1000次以内
        while ((lock == null || !lock) && count < 1000){
            lock = redisUtil.tryLock("submit_problem_" + taskDetailId, 3L, TimeUnit.MINUTES);
            ++count;
        }
        //	当前线程没有获取到锁,给出提示
        if(lock == null || !lock){
            return R.fail("当前道路正在被提交问题中,请稍后再试");
        }
        try{
            //	业务代码
        }catch (Exception e){
            throw e;
        } finally {
        	//	释放锁
            redisUtil.deleteKey("submit_problem_" + taskDetailId);
        }
        return R.ok("保存成功");
    }

对上面的代码的解释:

  1. 自循环: 多个线程同时进入方法,获取一把锁的时候,可能会导致竞争条件产生,使得几个线程都没有获取锁,这时,就需要进入自循环来再次获取锁。

  2. 竞争条件 :相当于三个人抢东西,三人互相阻碍,抢了一会儿之后,结果谁都没拿到。

  3. 结果分析 :最坏的情况就是,进入自循环后,仍然有不断的竞争条件产生,最后搞得所有的线程都没有拿到锁。否则,只有一个线程拿到锁,完成对数据的操作。

2.2.1 如果是某个集合需要做并发操作

上面的存储在 redis 里的键值换成一个集合就好


2.3 redis 工具类

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class RedisUtil {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 添加 Key 缓存
     *
     * @param key   String key
     * @param value Object
     * @param <T>   Value Type
     */
    public <T> void setKey(String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 添加 Key 缓存,并设置失效时间
     *
     * @param key   String key
     * @param value Object
     * @param time  Time
     * @param unit  TimeUnit
     * @param <T>   Value Type
     */
    public <T> void setKey(String key, final T value, long time, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, time, unit);
    }


    /**
     * 批量添加 Key 缓存
     *
     * @param valuesMap Map String:Object
     * @param <T>       Value Type
     */
    public <T> void setKey(Map<String, T> valuesMap) {
        redisTemplate.opsForValue().multiSet(valuesMap);
    }

    /**
     * 批量添加 Key 缓存,并设置失效时间
     *
     * @param valueMap     Map String:Object
     * @param expireMillis Map String:Long
     * @param <T>          Value Type
     */
    public <T> void setKey(Map<String, T> valueMap, Map<String, Long> expireMillis) {
        redisTemplate.opsForValue().multiSet(valueMap);
        setExpire(expireMillis);
    }

    /**
     * 获取 Key 缓存
     *
     * @param key String key
     * @param <T> Value Type
     * @return T
     */
    public <T> T getKey(final String key) {
        ValueOperations<String, T> operations = redisTemplate.opsForValue();
        return operations.get(key);
    }

    /**
     * 批量获取 Key 缓存值
     *
     * @param keys String key array
     * @param <T>  Value Type
     * @return T Array
     */
    public <T> List<T> getKey(List<String> keys) {
        ValueOperations<String, T> operations = redisTemplate.opsForValue();
        return operations.multiGet(keys);
    }

    /**
     * 判断 Key 是否存在
     *
     * @param key String key
     * @return boolean
     */
    public boolean hasKey(String key) {
        Boolean hasKey = redisTemplate.hasKey(key);
        return Boolean.TRUE.equals(hasKey);
    }

    /**
     * 批量获取 Key 缓存
     *
     * @param pattern Key pattern
     * @return Key Set
     */
    public Set<String> getKeys(final String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 删除 Key 缓存
     *
     * @param key Key
     */
    public void deleteKey(String key) {
        redisTemplate.delete(key);
    }

    /**
     * 批量删除 Key 缓存
     *
     * @param keys Key Array
     */
    public void deleteKey(List<String> keys) {
        redisTemplate.delete(keys);
    }

    /**
     * 指定键值失效时间
     *
     * @param key  String key
     * @param time Time
     * @param unit TimeUnit
     */
    public void setExpire(String key, long time, TimeUnit unit) {
        if (time > 0) {
            redisTemplate.expire(key, time, unit);
        }
    }

    /**
     * 批量指定键值失效时间
     *
     * @param expireMillis Map String:Long
     */
    public void setExpire(Map<String, Long> expireMillis) {
        if (null != expireMillis && expireMillis.size() > 0) {
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            redisTemplate.execute((RedisCallback<Object>) connection -> {
                expireMillis.forEach((key, expire) -> {
                    byte[] serialize = stringRedisSerializer.serialize(key);
                    if (null != serialize) {
                        connection.pExpire(serialize, expire);
                    }
                });
                return null;
            });
        }
    }

    /**
     * 指定键值在指定时间失效
     *
     * @param key  String key
     * @param date Date
     */
    public void setExpireAt(String key, Date date) {
        Date current = new Date();
        if (date.getTime() >= current.getTime()) {
            redisTemplate.expireAt(key, date);
        }
    }

    /**
     * 获取 Key 失效时间
     *
     * @param key  String key
     * @param unit TimeUnit
     * @return 剩余失效时长
     */
    public long getExpire(String key, TimeUnit unit) {
        Long expire = redisTemplate.getExpire(key, unit);
        if (null != expire) {
            return expire;
        }
        return 0L;
    }

    public Boolean tryLock(String key, Long expireTime, TimeUnit timeUnit){
        return redisTemplate.opsForValue().setIfAbsent(key, "", expireTime, timeUnit);
    }

    public Long realeaseLock(String key){
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        return (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), "");
    }

    public Long realeaseLocks(List<String> keys){
        String luaScript = "for _, key in ipairs(KEYS) do redis.call('del', key)  end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        return (Long) redisTemplate.execute(redisScript, keys, "");
    }

    public Long loginIncrement(String key){
        return redisTemplate.opsForValue().increment(key);
    }

}
  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值