Learn Redis 5 (Java)

发布于:2025-03-18 ⋅ 阅读:(13) ⋅ 点赞:(0)

分布式锁

在面对高并发业务时,单个项目解决不过来,此时一个项目部署到多个机器,这就是集群模式,不同的项目实例就会对应不同的端口和JVM。

1.模拟集群模式        

Nginx实现负载均衡(轮询)

2.使用集群模式下的问题 

使用集群模式,不同的项目则对应不同的JVM,而锁监视器是基于JVM的,这是就会存在线程安全问题,代码相同,申请的却不是同一把锁,一把是JVM1的,另一把是JVM2的。

在解决一人抢购多个商品业务(限购)时,出现业务数据异常:

同一个人连续发送两次请求,一个到了8081、另一个在8082,都能获取到锁,查询得到的也是同一个结果,导致两个不同实例线程都能购买成功。

3.分布式锁 

造成这个的原因是锁不是共享的,且不可视,JVM1看不到JVM2的锁。

解决:使他们申请的是同一把锁,实现多线程之间互斥

使用分布式锁,使不同的JVM统一锁监视器 

 4.使用redis实现分布式锁

使用逻辑和Lock基本一致,但是获取锁失败后不会等待而是返回false,且添加了超时释放

    public boolean tryLock(String key){
        Boolean flat = redisTemplate.opsForValue().setIfAbsent(key, "1", TTL, TimeUnit.SECONDS);
        return flat;
    }

    public void unLock(String key){
        redisTemplate.delete(key);
    }

问题 

1.锁被误删

存在线程1 tryLock()成功后,执行业务逻辑,但是发生了业务阻塞,就卡在那了,导致没有手动的 unLock(),而是因为超时而释放了,

此时线程2进来了 ,tryLock()成功后,执行业务逻辑,线程1突然好了,就去释放锁了,但是此时的锁不是线程1的,而是线程2的,线程2执行完后很懵逼,家怎么被偷了,我该释放谁啊?

解决:释放锁时增加判断,锁是自己的吗 

    public boolean tryLock(Long expireTime){
        // 获取当前线程id
        // 在集群中,线程id由所在jvm管理,所以线程id会重复
        long id = Thread.currentThread().getId();
        String value = uuid+id;
        // 设置锁
        Boolean succeed = stringRedisTemplate.opsForValue().setIfAbsent(
                prefix + name, value, expireTime, TimeUnit.SECONDS);
        // 拆箱
        return Boolean.TRUE.equals(succeed);
    }


   public void unLock(){
        // 获取当前线程value
        String value = stringRedisTemplate.opsForValue().get(prefix + name);
        // 判断是否是自己的锁
        long id = Thread.currentThread().getId();
        String myValue = uuid+id;
        if (value.equals(myValue)){
            stringRedisTemplate.delete(prefix + name);
        }
    }

 这就行了吗,会不会有更加极端的情况,卡在了stringRedisTemplate.delete(prefix + name),判断锁逻辑已经成功,要删除时,业务阻塞了,又发生上面的锁被误删情况。

原因是这个操作不是原子性的,所以存在中途发生问题,只需要把操作写到一个脚本

2.Lua脚本

使用redis命令调用Lua脚本Lua 教程 | 菜鸟教程

基本全局变量a=10,局部变量local b=10,方法function,调用redis redis.call()

1 EVAL script numkeys key [key ...] arg [arg ...]
执行 Lua 脚本。
2 EVALSHA sha1 numkeys key [key ...] arg [arg ...]
执行 Lua 脚本。
3 SCRIPT EXISTS script [script ...]
查看指定的脚本是否已经被保存在缓存当中。
4 SCRIPT FLUSH
从脚本缓存中移除所有脚本。
5 SCRIPT KILL
杀死当前正在运行的 Lua 脚本。
6 SCRIPT LOAD script
将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本。

 主要看1,EVAL  script(脚本)numkeys(key参数数量)key[](key参数),arg[](其他参数)

写Lua脚本

IDEA下载EmmyLua插件,创建Lua脚本在resource路径下

if (redis.call('exists', KEYS[1]) == ARGV[1]) then
    redis.call('del', KEYS[1])
end
return 0

 调用Lua脚本

    //定义释放锁的lua脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static{
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public void unLock(){
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                //使用Collections.singletonList(),将key转换为list
                Collections.singletonList(prefix + name),
                uuid+Thread.currentThread().getId()
        );
    }

5.Redisson

1.添加maven依赖
        <!--Redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.2</version>
        </dependency>
2.配置Redisson客户端
@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient(){
        // 创建配置
        Config config = new Config();
        // 添加节点信息, ip:port和密码
        config.useSingleServer().setAddress("redis://localhost:6379").setPassword("123456");
        return Redisson.create(config);
    }
}
3.修改业务
        //使用redisson获取锁对象,可重入
        RLock lock = redissonClient.getLock("order:" + id);
        //尝试获取锁,尝试获取锁的时间最大等待时间为1s,超时释放时间20s
        boolean isLock = lock.tryLock(1,20L, TimeUnit.SECONDS);
        if (!isLock){
            return Result.fail("不允许重复下单");
        }
        try {
            //手动创建代理对象
            IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }