Redis实现分布式锁

本文详细介绍了如何使用Redis实现分布式锁,包括原理、测试过程及遇到的问题。通过设置锁的过期时间和使用Lua脚本确保原子性,解决了死锁和安全性问题,实现了更可靠的分布式锁解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

Redis实现分布式锁

原理

测试代码

测试结果 

问题

优化一

解决办法

测试代码

测试结果

问题

优化二

解决办法

测试代码

测试结果


Redis实现分布式锁

原理

Redis中有这么个东西(key:k1 value:v1),k1这个key就是判断的锁

        当线程进来访问时,判断redis中是否有k1,如果没有,就设置k1然后访问资源,操作,此时另一个线程过来再次获取是否存在k1,当然此时k1肯定是存在的,所以该线程不能访问锁的资源,需要等待或者重新访问。

        当访问的线程访问完之后要释放k1也就是删除k1。

测试代码

package com.shao.seckill;

import com.shao.seckill.utils.UUIDUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class SeckillApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisScript<Boolean> script;

    @Test
    void contextLoads() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //占位,如果key不存在才可以设置成功返回true
        Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
        //如果占位成功,进行正常操作
        if(isLock){
            //操作
            valueOperations.set("name","xxxx");
            String name = (String) valueOperations.get("name");
            System.out.println(name);
            //操作完之后删除锁,让其它线程能够使用
            redisTemplate.delete("k1");
        }else{
            System.out.println("有线程在使用,请稍后尝试");
        }
    }

}

测试结果 

        可以正常锁和解锁资源

问题

        看似没有问题其实有很大隐患,如果一个线程抢占资源之后没有释放资源就抛出异常,此时就出现死锁现象,别的线程再也等不到资源。

优化一

解决办法

        给锁的资源添加过期时间,例如该部分执行的时间是3秒,我们就设置k1的过期时间是5秒,此时我们人为的设置个异常,就算第一个线程进入后抛出异常过五秒之后也k1也会自动失效。

测试代码

package com.shao.seckill;

import com.shao.seckill.utils.UUIDUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class SeckillApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisScript<Boolean> script;


@Test
    void contextLoads02(){
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //比上次多设置一个时间,到时见自动解锁,解决出现死锁的情况
        //但是此种方法并不安全
        //  例如A线程获取锁,由于某些原因在执行操作时耗费时间特别长
        //  但是设置锁的key已经到达自动销毁时间,下一个线程B就能获取锁进入该操作
        //  此时线程A在删除锁,其实删除的是线程B的锁,至此连续错误删除加锁导致系统出错
        Boolean isLock = valueOperations.setIfAbsent("k1", "v1",5, TimeUnit.SECONDS);
        if(isLock){
            valueOperations.set("newname","newxxxx");
            String name = (String) valueOperations.get("newname");
            System.out.println(name);
            //人为制造异常
            int x =5/0;
            redisTemplate.delete("k1");
        }else{
            System.out.println("有线程在使用,请稍后尝试");
        }
    }
}

测试结果

        解决死锁问题

问题

        还是不安全

        例如A线程获取锁,由于某些原因在执行操作时耗费时间特别长
        但是设置锁的key已经到达自动销毁时间,下一个线程B就能获取锁进入该操作
        此时线程A再删除锁,其实删除的是线程B的锁,至此连续错误删除加锁导致系统出错

优化二

解决办法

        我们可以为k1设置随机数,使每个线程的锁值都不相同,解锁时需验证k1的值。此时又出现新的问题,因为此时解锁就需要先验证k1的值再删除k1,无法保证原子性,这时我们就需要利用lua脚本来实现一次性执行整个验k1的值和删除k1的。

测试代码

准备lock.lua脚本在resources下和application.properties(或者application.yaml)同级

if redis.call("get",KEYS[1])==ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

编写对应的Redis配置

package com.shao.seckill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

    @Bean
    public DefaultRedisScript defaultRedisScript(){
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //lock.lua脚本位置和application.yaml同级目录
        redisScript.setLocation(new ClassPathResource("lock.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }

}

测试使用

package com.shao.seckill;

import com.shao.seckill.utils.UUIDUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class SeckillApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisScript<Boolean> script;

@Test
    void contextLoads03(){
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //针对以上出现的问题,可以通过将锁的值编程随机数
        //线程对于锁就需要验证锁,解锁这几个步骤
        //这些步骤要满足原子性不然会出先错误
        //LUA脚本让多个命令一次进行
        // 1.在Redis服务器中写LUA脚本:服务器压力大,每次都运行都会携带该脚本
        // 2.在java客户端中写LUA脚本:每次都需要发送给Redis服务器中,通信时间长
        //编写RedisConfig设置要使用脚本lock.lup的位置
        String value = UUID.randomUUID().toString();
        Boolean isLock = valueOperations.setIfAbsent("k1", value,5, TimeUnit.SECONDS);
        if(isLock){
            valueOperations.set("newnewname","newxxxx");
            String name = (String) valueOperations.get("newnewname");
            System.out.println(name);
            System.out.println(valueOperations.get("k1"));
            //执行lua脚本 验证锁值 删除锁
            Boolean result = (Boolean) redisTemplate.execute(script, Collections.singletonList("k1"), value);
            System.out.println(result);
        }else{
            System.out.println("有线程在使用,请稍后尝试");
        }
    }
}

测试结果

        解决以上问题,能进行解锁和加锁等操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值