SpringBoot 整合 Redis 实现分布式锁

本贴最后更新于 1489 天前,其中的信息可能已经时异事殊

本文主要为了回顾一下 Java 对 Redis 的操作,对 SpringBoot 自带的 Redis 组件进行简单的封装,实现一个基础版的分布式锁。后续会进行扩展。话不多说,直接进入正题。

1.配置文件

POM 文件:

<dependencies>
        <!-- cache -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

</dependencies>

application.yml 文件:

spring:
  ## redis 配置
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    ## 连接 redis 超时时间
    timeout: 3000ms
    jedis:
      ## redis 连接池配置
      pool:
        ## 最大空闲连接数
        max-idle: 20
        ## 最小空闲连接数
        min-idle: 0
        ## 最长阻塞等待时间
        max-wait: 30
        ## 最大活跃连接数
        max-active: 15

2.配置 RedisTemplate

@Configuration
public class RedisConfig
{
    /**
     * 配置RedisTemplate,设置key和value的序列化方式
     *
     * @return RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);

        // 配置序列化
        setSerializer(redisTemplate);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    /**
     * 配置 RedisTemplate 序列化
     *
     * @param redisTemplate redisTemplate
     */
    @SuppressWarnings("unchecked")
    private void setSerializer(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //noinspection rawtypes,unchecked
        Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jsonRedisSerializer.setObjectMapper(om);

        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jsonRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);
    }
}

这里如果不设置 RedisTemplate 的序列化方式话,默认会使用 JdkSerializationRedisSerializer,虽然对使用上没什么影响,但数据的可读性会比较差。(可以使用默认的序列化器体验一下)


3.简单地封装常用功能

@SuppressWarnings("unchecked")
@Component
public class RedisTool {

    private Logger logger = LoggerFactory.getLogger(RedisTool.class);

    @Autowired
    @SuppressWarnings("rawtypes")
    private RedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 添加缓存
     *
     * @param key   key
     * @param value 缓存内容
     */
    public void put(Object key, Object value) {
        if (verifyParam(key, value)) {
            redisTemplate.opsForValue().set(key, value);;
        }
    }

    /**
     * 添加缓存
     *
     * @param key        key
     * @param value      缓存内容
     * @param expireTime 过期时间
     */
    public void put(Object key, Object value, Long expireTime) {
        if (verifyParam(key, value)) {
            redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(expireTime));
        }
    }

    /**
     * 添加缓存
     *
     * @param key        key
     * @param value      value
     * @param expireTime 过期时间
     * @param timeUnit   时间单位
     */
    public void put(Object key, Object value, Long expireTime, TimeUnit timeUnit) {
        if (verifyParam(key, value)) {
            redisTemplate.opsForValue().set(key, value, expireTime, timeUnit);
        }
    }

    /**
     * 获取 对象
     *
     * @param key   key
     * @param clazz 对象class
     * @param <T>   对象泛型
     * @return 对象
     */
    public <T> T get(Object key, Class<T> clazz) {
        if (verifyParam(key)) {
            Object value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                try {
                    String jsonStr = objectMapper.writeValueAsString(value);
                    return objectMapper.readValue(jsonStr, clazz);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 获取 字符串
     *
     * @param key key
     * @return 字符串
     */
    public String get(Object key) {
        if (verifyParam(key)) {
            Object o = redisTemplate.opsForValue().get(key);
            return o == null ? null : String.valueOf(o);
        }
        return null;
    }

    /**
     * 删除缓存
     *
     * @param key key
     * @return 是否删除成功
     */
    public Boolean delete(Object key) {
        if (verifyParam(key)) {
            return redisTemplate.delete(key);
        }
        return Boolean.FALSE;
    }

    /**
     * 如果缓存不存在 添加
     *
     * @param key   key
     * @param value value
     */
    public void setIfAbsent(Object key, Object value) {
        if (verifyParam(key, value)) {
            redisTemplate.opsForValue().setIfAbsent(key, value);
        }
    }

    /**
     * 如果缓存不存在 添加
     *
     * @param key        key
     * @param value      value
     * @param expireTime 过期时间
     */
    public void setIfAbsent(Object key, Object value, long expireTime) {
        if (!verifyParam(key, value)) {
            return;
        }
        redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(expireTime));
    }

    /**
     * 检查 键值对合法性
     *
     * @param key   key
     * @param value value
     * @return 是否合法
     */
    private boolean verifyParam(Object key, Object value) {
        if (key == null || value == null) {
            logger.warn("Illegal param, null key or null value.");
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    /**
     * 检查 key 合法性
     *
     * @param key key
     * @return 是否合法
     */
    private boolean verifyParam(Object key) {
        if (key == null) {
            logger.warn("Illegal param, null key.");
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }
}

4. 锁的实现

抽象锁 RedisLock,尝试获取锁和释放锁具体由其子类实现。

public abstract class RedisLock {

    public RedisTool redisTool;

    private String lockUUId;

    public RedisLock(RedisTool redisTool) {
        this.redisTool = redisTool;
    }

    public String getLockUUId() {
        return lockUUId;
    }

    public void setLockUUId(String lockUUId) {
        this.lockUUId = lockUUId;
    }

    /**
     * 加锁
     */
    public void acquire() {
        if (tryAcquire()) {
            redisTool.put(RedisTool.LOCK_NAME, lockUUId);
        }
    }

    /**
     * 加锁一定时间后自动释放锁
     *
     * @param leaseTime 超过该时间,自动释放锁
     * @param timeUnit  时间单位
     */
    public void acquire(Long leaseTime, TimeUnit timeUnit) {
        if (tryAcquire()) {
            redisTool.put(RedisTool.LOCK_NAME, lockUUId, leaseTime, timeUnit);
        }
    }

    /**
     * 尝试加锁
     *
     * @return 是否可以加锁
     */
    public abstract Boolean tryAcquire();

    /**
     * 释放锁
     */
    public void release() {
        if (tryRelease()) {
            redisTool.delete(RedisTool.LOCK_NAME);
        }
    }

    /**
     * 尝试释放锁
     *
     * @return 是否可以释放锁
     */
    public abstract Boolean tryRelease();
}

SimpleRedisLock:锁的简单实现

public class SimpleRedisLock extends RedisLock {
    private Logger logger = LoggerFactory.getLogger(SimpleRedisLock.class);

    private Long waitTime = 3000L;

    public SimpleRedisLock(RedisTool redisTool) {
        super(redisTool);
    }

    @Override
    public Boolean tryAcquire() {
        long startMills = System.currentTimeMillis();
        String lockUUID = getLockUUId();
        logger.debug("{} try to acquire the lock {}", Thread.currentThread().getName(), lockUUID);
        while (System.currentTimeMillis() - startMills < waitTime) {
            String existLock = redisTool.get(RedisTool.LOCK_NAME);
            if (!(StringUtils.isNotBlank(existLock) && StringUtils.equals(lockUUID, existLock))) {
                logger.debug("lock {} can be acquired", lockUUID);
                return Boolean.TRUE;
            }
        }
        logger.debug("{} try to acquire lock {} failed", Thread.currentThread().getName(), lockUUID);
        return Boolean.FALSE;
    }

    @Override
    public Boolean tryRelease() {
        String lockUUID = getLockUUId();
        String existLock = redisTool.get(RedisTool.LOCK_NAME);
        if (StringUtils.isBlank(existLock) || !StringUtils.equals(lockUUID, existLock)) {
            logger.debug("lock {} has been released", lockUUID);
            return Boolean.TRUE;
        }
        if (StringUtils.equals(lockUUID, existLock)) {
            logger.debug("lock {} can be released", lockUUID);
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
}

6.测试锁的功能

测试类:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class TestSimpleRedisLock
{
    private Logger logger = LoggerFactory.getLogger(TestSimpleRedisLock.class);

    @Autowired
    private RedisTool redisTool;

    @Test
    public void testSimpleRedisLock() throws InterruptedException
    {
        RedisLock lock = new SimpleRedisLock(redisTool);
        lock.setLockUUId(UUID.randomUUID().toString());

        // 2秒后自动释放锁
        lock.acquire(2L, TimeUnit.SECONDS);
        logger.info("main acquired lock {}", lock.getLockUUId());

        Thread thread = new Thread(() -> {
            lock.acquire();
            logger.info("thread acquire lock {}", lock.getLockUUId());
            lock.release();
            logger.info("thread release lock {}", lock.getLockUUId());
        });

        thread.start();
        // 手动释放锁,
        lock.release();
        logger.info("main release lock {}", lock.getLockUUId());
        thread.join();
    }
}

测试结果:

20:08:10.047  INFO 11020 --- [           main] com.demo.redis.core.TestSimpleRedisLock  : main acquired lock 78cd8137-90f1-4830-9509-ff69b718083f
20:08:12.085  INFO 11020 --- [       Thread-3] com.demo.redis.core.TestSimpleRedisLock  : thread acquire lock 78cd8137-90f1-4830-9509-ff69b718083f
20:08:12.157  INFO 11020 --- [       Thread-3] com.demo.redis.core.TestSimpleRedisLock  : thread release lock 78cd8137-90f1-4830-9509-ff69b718083f

后记

代码中还有很多地方不够严谨,例如获取锁超时,失败重试等机制的实现,本文主要讨论的是分布式锁实现的思想,如有不当欢迎指正。

源码地址:https://github.com/NekoChips/SpringDemo/09.springboot-redis

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    941 引用 • 1458 回帖 • 150 关注
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    284 引用 • 247 回帖 • 174 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...