业务使用redis分布式锁

伴随着业务体量的上升,我们的qps与并发问题越来越明显,这时候就需要用到让代码一定情况下进行串行执行的工具:锁

1.业务场景代码

java 复制代码
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Object testBatch(User user) {
        LambdaQueryWrapper<User> eq = Wrappers.<User>lambdaQuery()
                .eq(User::getBatch, user.getBatch());
        List<User> userList = list(eq);
        if (CollUtil.isEmpty(userList)) {
            save(user);
        } else {
            this.lambdaUpdate().eq(User::getBatch, user.getBatch())
                    .set(User::getUsername, user.getUsername())
                    .set(User::getUpdateTime, LocalDateTime.now())
                    .update();
        }
        redisUtil.delete(key);
        return user;
    }

备注:上述的代码逻辑在串行执行的时候是没有任何问题的,但是假如同时有两个线程进来:两个线程同时读取到当前batch对应的user为null,那么此时当前两个线程就会同时执行insert语句,导致当前batch本该只有1个user的但是此刻数据库有2个user记录。这个就是并发问题

2.解决方案

此刻我能想到的解决方案有以下三种,此处只讲redis锁

2.1 代码同步执行

2.1.1 redis分布式锁

java 复制代码
    private int maxCostSeconds = 5;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Object testBatch(User user) {
        String key = "userBatch::" + user.getBatch();
        boolean lock = redisUtil.setNxEx(key, key, maxCostSeconds);
        LocalDateTime startNow = LocalDateTime.now();
        LocalDateTime endNow = LocalDateTime.now();
        // 自选等待获取锁,超过5s就放弃
        int count = 0;
        while (!lock) {
            lock = redisUtil.setNxEx(key, key, maxCostSeconds);
            if (lock) {
                break;
            }
            endNow = LocalDateTime.now();
            int costSeconds = endNow.getSecond() - startNow.getSecond();
            if (costSeconds >= maxCostSeconds) {
                break;
            }
            Thread.sleep(500);
            System.out.println("获取次数:" + count++);
        }
        System.out.println("当前线程获取到了 redis锁,线程名" + Thread.currentThread().getName());
        if (!lock) {
            throw new RunTimeException("系统繁忙,请稍后重试");
        }
        LambdaQueryWrapper<User> eq = Wrappers.<User>lambdaQuery()
                .eq(User::getBatch, user.getBatch());
        List<User> userList = list(eq);
        if (CollUtil.isEmpty(userList)) {
            save(user);
        } else {
            this.lambdaUpdate().eq(User::getBatch, user.getBatch())
                    .set(User::getUsername, user.getUsername())
                    .set(User::getUpdateTime, LocalDateTime.now())
                    .update();
        }
        redisUtil.delete(key);
        return user;
    }

redis工具类

java 复制代码
package com.lzq.learn.utils;

import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;


@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    private final RedisScript<String> lockScript = new DefaultRedisScript<>("if redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then return ARGV[1] else return nil end", String.class);
    private final RedisScript<Long> unlockScript = new DefaultRedisScript<>("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class);

    public boolean acquireLock(String lockKey, String lockValue, long expireTime) {
        String result = redisTemplate.execute(lockScript, Collections.singletonList(lockKey), lockValue, expireTime);
        return "OK".equals(result);
    }

    public void releaseLock(String lockKey, String lockValue) {
        redisTemplate.execute(unlockScript, Collections.singletonList(lockKey), lockValue);
    }
    /**
     * 删除key
     *
     * @param key
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    /**
     * 批量删除key
     *
     * @param keys
     */
    public void delete(Collection<String> keys) {
        redisTemplate.delete(keys);
    }

    /**
     * set NX  PX
     * @param key key
     * @param value value
     * @param seconds 过期时间  单位:seconds
     * @return boolean
     */
    public boolean setNxEx(String key , String value , int seconds){
        Boolean result = false;
        try {
            result = redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                    RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
                    RedisSerializer keySerializer = redisTemplate.getKeySerializer();
                    try {
                        Object set = redisConnection.execute("set", keySerializer.serialize(key), value.getBytes("UTF-8")
                                , "NX".getBytes("UTF-8"), "EX".getBytes("UTF-8"),
                                String.valueOf(seconds).getBytes("UTF-8"));
                        return "OK".equals(String.valueOf(set));
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                        return false;
                    }
                }
            });
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
        return result;
    }

}

2.1.2 java锁(synchronized、lock)

2.2 数据库唯一索引校验

2.3 数据库锁(select for update行锁,version乐观锁)

相关推荐
杜子不疼.12 分钟前
计算机视觉热门模型手册:Spring Boot 3.2 自动装配新机制:@AutoConfiguration 使用指南
人工智能·spring boot·计算机视觉
indexsunny2 小时前
互联网大厂Java求职面试实战:Spring Boot微服务与Redis缓存场景解析
java·spring boot·redis·缓存·微服务·消息队列·电商
DBA小马哥2 小时前
时序数据库迁移替换与时序数据库分片
数据库·时序数据库
DBA小马哥2 小时前
时序数据库迁移方案在物联网设备监测中的实践与性能突破
数据库·物联网·时序数据库
ID_180079054732 小时前
小红书笔记详情API接口基础解析:数据结构与调用方式
数据结构·数据库·笔记
无心水2 小时前
【分布式利器:腾讯TSF】7、TSF高级部署策略全解析:蓝绿/灰度发布落地+Jenkins CI/CD集成(Java微服务实战)
java·人工智能·分布式·ci/cd·微服务·jenkins·腾讯tsf
28岁青春痘老男孩7 小时前
JDK8+SpringBoot2.x 升级 JDK 17 + Spring Boot 3.x
java·spring boot
ruleslol8 小时前
MySQL的段、区、页、行 详解
数据库·mysql
独自归家的兔8 小时前
Spring Cloud核心架构组件深度解析(原理+实战+面试高频)
spring cloud·面试·架构
天若有情6738 小时前
校园二手交易系统实战开发全记录(vue+SpringBoot+MySQL)
vue.js·spring boot·mysql