SpringBoot使用Redis实现分布式锁

一、Redis分布式锁概念篇

建议直接采用Redis的官方推荐的Redisson作为redis的分布式锁

1.1、为什么要使用分布式锁

 我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程的18般武艺进行处理,并且可以完美的运行,毫无Bug!

    注意这是单机应用,也就是所有的请求都会分配到当前服务器的JVM内部,然后映射为操作系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间!

    后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:

SpringBoot使用Redis实现分布式锁_第1张图片

    上图可以看到,变量A存在JVM1、JVM2、JVM3三个JVM内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象,例如:UserController控制器中的一个整形类型的成员变量),如果不加任何控制的话,变量A同时都会在JVM分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!

    如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!

    为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

 1.2、分布式锁应具备哪些条件

         在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:

 1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;

  2、高可用的获取锁与释放锁;

  3、高性能的获取锁与释放锁;

  4、具备可重入特性;

  5、具备锁失效机制,防止死锁;

  6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

1.3、分布式锁的三种实现方式

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

    在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。

    1、基于数据库实现分布式锁;

    2、基于缓存(Redis等)实现分布式锁;

    3、基于Zookeeper实现分布式锁;

二、Redis分布式锁实战篇

2.1、导入依赖


    org.springframework.boot
    spring-boot-starter-redis
    1.4.7.RELEASE

2.2、配置Redis配置信息


spring.redis.host=127.0.0.1
spring.redis.port=6380
spring.redis.database=0
spring.redis.password=123456
spring.redis.timeout=10000

# 设置jedis连接池
spring.redis.jedis.pool.max-active=50
spring.redis.jedis.pool.min-idle=20

yml配置文件

spring
    redis:
      port: 6379
      host: 127.0.0.1
      password:
      database: 0

2.3、配置RedisConfig属性


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws Exception {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 3.创建 序列化类
        GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(genericToStringSerializer);
//        redisTemplate.setKeySerializer(new StringRedisSerializer());
//        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

2.4、写一个RedisLock工具类



import com.alibaba.fastjson.JSON;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class RedisLock {


    @Resource
    private RedisTemplate redisTemplate;


    private static Map lockInfoMap = new ConcurrentHashMap<>();

    private static final Long SUCCESS = 1L;


    @Data
    public static class LockInfo {

        private String key;

        private String value;

        private int expireTime;

        //更新时间
        private long renewalTime;

        //更新间隔
        private long renewalInterval;

        public static LockInfo getLockInfo(String key, String value, int expireTime) {
            LockInfo lockInfo = new LockInfo();
            lockInfo.setKey(key);
            lockInfo.setValue(value);
            lockInfo.setExpireTime(expireTime);
            lockInfo.setRenewalTime(System.currentTimeMillis());
            lockInfo.setRenewalInterval(expireTime * 2000 / 3);
            return lockInfo;
        }
    }


    /**
     * Lua脚本
     * // 加锁
     * if
     *     redis.call('setNx',KEYS[1],ARGV[1])
     *   then
     *     if redis.call('get',KEYS[1])==ARGV[1]
     *     return redis.call('expire',KEYS[1],ARGV[2])
     *   else
     *     return 0
     *   end
     * end
     *
     * // 解锁
     *   redis.call('get', KEYS[1]) == ARGV[1]
     * then
     *   return redis.call('del', KEYS[1])
     * else
     *   return 0
     *
     *
     *   //更新时间
     *   if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end
     *
     *
     *
     *
     */


    /**
     * 使用lua脚本加锁
     *
     * @param lockKey    锁
     * @param value      身份标识(保证锁不会被其他人释放)
     * @param expireTime 锁的过期时间(单位:秒)
     * @Desc 注意事项,redisConfig配置里面必须使用 genericToStringSerializer序列化,否则获取不了返回值
     */
    public boolean tryLock(String lockKey, String value, int expireTime) {

        String luaScript = "if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end end";

        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Boolean.class);
        redisScript.setScriptText(luaScript);
        List keys = new ArrayList<>();
        keys.add(lockKey);
        //Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey),value,expireTime + "");
        // Object result = redisTemplate.execute(redisScript, new StringRedisSerializer(), new StringRedisSerializer(), Collections.singletonList(lockKey), identity, expireTime);

        Object result = redisTemplate.execute(redisScript, keys, value, expireTime);
        log.info("已获取到{}对应的锁!", lockKey);

        if (expireTime >= 10) {
            lockInfoMap.put(lockKey + value, LockInfo.getLockInfo(lockKey, value, expireTime));
        }

        return (boolean) result;
    }


    /**
     * 使用lua脚本释放锁
     *
     * @param lockKey
     * @param value
     * @return 成功返回true, 失败返回false
     */
    public boolean unlock(String lockKey, String value) {

        lockInfoMap.remove(lockKey + value);

        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Boolean.class);
        redisScript.setScriptText(luaScript);
        List keys = new ArrayList<>();
        keys.add(lockKey);
        Object result = redisTemplate.execute(redisScript, keys, value);
        log.info("解锁成功:{}", result);
        return (boolean) result;
    }


    /**
     * 使用lua脚本更新redis锁的过期时间
     *
     * @param lockKey
     * @param value
     * @return 成功返回true, 失败返回false
     */
    public boolean renewal(String lockKey, String value, int expireTime) {

        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
        DefaultRedisScript redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Boolean.class);
        redisScript.setScriptText(luaScript);
        List keys = new ArrayList<>();
        keys.add(lockKey);

        Object result = redisTemplate.execute(redisScript, keys, value, expireTime);
        log.info("更新redis锁的过期时间:{}", result);
        return (boolean) result;
    }


    /**
     * redisTemplate加锁
     *
     * @param lockKey    锁
     * @param value      身份标识(保证锁不会被其他人释放)
     * @param expireTime 锁的过期时间(单位:秒)
     * @return 成功返回true, 失败返回false
     */
    public boolean lock(String lockKey, String value, long expireTime) {
        return redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);

    }


    /**
     * redisTemplate解锁
     *
     * @param key
     * @param value
     * @return 成功返回true, 失败返回false
     */
    public boolean unlock2(String key, String value) {
        Object currentValue = redisTemplate.opsForValue().get(key);
        boolean result = false;
        if (StringUtils.isNotEmpty(String.valueOf(currentValue)) && currentValue.equals(value)) {
            result = redisTemplate.opsForValue().getOperations().delete(key);
        }
        return result;
    }


    /**
     * @author liuminglin
     * @Date 2021/11/19 17:08
     * @Desc 定时去检查redis锁的过期时间
     * @Param
     * @Return
     */
    @Scheduled(fixedRate = 5000L)
    @Async("redisExecutor")
    public void renewal() {
        long now = System.currentTimeMillis();
        for (Map.Entry lockInfoEntry : lockInfoMap.entrySet()) {
            LockInfo lockInfo = lockInfoEntry.getValue();
            if (lockInfo.getRenewalTime() + lockInfo.getRenewalInterval() < now) {
                renewal(lockInfo.getKey(), lockInfo.getValue(), lockInfo.getExpireTime());
                lockInfo.setRenewalTime(now);
                log.info("lockInfo {}", JSON.toJSONString(lockInfo));
            }
        }
    }

    /**
     * @author liuminglin
     * @Date 2021/11/19 17:08
     * @Desc 分布式锁设置单独线程池
     * @Param
     * @Return
     */
    @Bean("redisExecutor")
    public Executor redisExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(1);
        executor.setQueueCapacity(1);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("redis-renewal-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
        return executor;
    }
}

开启定时任务

在启动类上加 @EnableScheduling 注解,定时任务才会开启


@SpringBootApplication
@MapperScan(value = "com.example.recordlog.mapper")
@EnableAspectJAutoProxy(proxyTargetClass = true)
//开启定时任务
@EnableScheduling
public class RecordLogApplication {

    public static void main(String[] args) {
        SpringApplication.run(RecordLogApplication.class, args);
    }

}

2.5、测试接口



import com.example.recordlog.tools.RedisLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/api")
public class OperateController {

   
@Autowired
private RedisLock redisLock;


@RequestMapping(value = "/locked", method = {RequestMethod.GET})
public void lockedTest() {

        String key = String.format("data-mining:task_statistic:%d", System.currentTimeMillis());
        String requestId = UUID.randomUUID().toString();

        boolean locked = false;
        try {
            locked = redisLock.tryLock(key, requestId, 30);
            if (!locked) {
                return;
            }
            //执行业务逻辑
            System.out.println("---------------->>>执行业务逻辑");
        } finally {
            if (locked) {
                redisLock.unlock(key, requestId);
            }
        }

    }


}

你可能感兴趣的:(SpringBoot,spring,boot,redis)