Redis进阶(5)——Redis数据一致性 & 用Java代码加锁解决一致性 & 采用lua脚本实现减1的原子性 & 分布式环境下的setnex锁及其问题 & Redission框架的使用

不管现实多么惨不忍睹,都要持之以恒地相信,这只是黎明前短暂的黑暗而已。不要惶恐眼前的难关迈不过去,不要担心此刻的付出没有回报,别再花时间等待天降好运。真诚做人,努力做事!你想要的,岁月都会给你。Redis进阶(5)——Redis数据一致性 & 用Java代码加锁解决一致性 & 采用lua脚本实现减1的原子性 & 分布式环境下的setnex锁及其问题 & Redission框架的使用,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

引出


1.redis中数据一致性的问题,大量抢购如何保证数据安全;
2.用java代码加锁解决一致性问题,可重入锁,以及死锁的产生;
3.采用lua脚本,让库存-1操作原子化;
4.分布式环境下setnx锁,以及存在的问题;
5.Redisson框架的使用,锁的续期,看门狗策略,1/3时续期;

Redis中的数据一致性

CAP原则:

C:一致性

A: 可用性

P: 分区容错性(*) AP CP

BASE——最终一致性

场景

redis存储库存,多个客户(线程)请求, 库存量100, 如何保证数据一致性。

用Java代码加锁解决一致性

大量请求拥挤抢购

在这里插入图片描述

在这里插入图片描述

package com.tianju.redis.service.impl;

import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class RushGoodsServiceImpl implements IRushGoodsService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final String GOODS = "goods";

    @Override
    public String rush() {
        String sNum = stringRedisTemplate.opsForValue().get(GOODS);
        int nums = Integer.parseInt(sNum);
        if (nums>0){
            stringRedisTemplate.opsForValue().set(GOODS, String.valueOf(--nums) );
            return stringRedisTemplate.opsForValue().get(GOODS);
        }else {
            return "error";
        }
    }
}
    @PutMapping("/rushJmeter")
    public void rushJmeter(){
        String goodsNum = rushGoodsService.rush();
        System.out.println("goodsNum: "+goodsNum);
    }

在这里插入图片描述

加锁(java)

可重入锁: sychronized ReentrantLock

在这里插入图片描述

synchronized (this.getClass()){}

在这里插入图片描述

private final ReentrantLock lock = new ReentrantLock(); // 可重入锁

在这里插入图片描述

什么是可重入锁?

当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁;

在这里插入图片描述

在这里插入图片描述

如何保证可重入

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁

如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测试一下Mark Word中偏向锁标志是否设置成1:没有则CAS竞争;设置了,则CAS将对象头偏向锁指向当前线程。再维护一个计数器,同个线程进入则自增1,离开再减1,直到为0才能释放

滥用锁的代价?

死锁的四个必要条件?

循环

A —> B —>C —>A

在这里插入图片描述

可能导致死锁

在这里插入图片描述

锁对象ObjLock

package com.tianju.redis.lock;

public class ObjLock {
    private String name;

    public ObjLock(String name){
        this.name = name;
    }

    @Override
    public String toString() {
        return "ObjLock:"+this.name;
    }
}

加锁释放锁方法DeadLockDemo

package com.tianju.redis.lock;

public class DeadLockDemo {

    private ObjLock a;
    public ObjLock b;

    public DeadLockDemo(ObjLock a,ObjLock b){
        this.a = a;
        this.b = b;
    }

    public void dead(){
        System.out.println("********"+a+"对象"+b+"对象都加锁**************");
        System.out.println(a+"--"+b+": "+"准备给"+a+"对象加锁>>");
        synchronized (a){
            System.out.println(a+"--"+b+": "+a+"对象加锁成功...");
            System.out.println(a+"--"+b+": "+"准备给"+b+"对象加锁>>>");
            synchronized (b){
                System.out.println(a+"--"+b+": "+b+"对象加锁成功");
            }
            System.out.println(a+"--"+b+": "+"释放"+b+"对象的锁");
        }
        System.out.println(a+"--"+b+": "+"释放"+b+"对象的锁");
        System.out.println("****************");
    }
}

测试方法

package com.tianju.redis.lock;

public class TestDeadLock {

    /**
     * 一个一个顺序运行
     */
    public static void run(){
        ObjLock a = new ObjLock("A");
        ObjLock b = new ObjLock("B");
        ObjLock c = new ObjLock("C");

        DeadLockDemo lockDemo1 = new DeadLockDemo(a, b);
        lockDemo1.dead(); // 锁住a和b

        DeadLockDemo lockDemo2 = new DeadLockDemo(b, c);
        lockDemo2.dead(); // 锁住a和b

        DeadLockDemo lockDemo3 = new DeadLockDemo(c, a);
        lockDemo3.dead(); // 锁住a和b
    }

    /**
     * 进行线程抢,死锁
     */
    public static void rushRun(){
        ObjLock a = new ObjLock("A");
        ObjLock b = new ObjLock("B");
        ObjLock c = new ObjLock("C");

        new Thread(()->{
            DeadLockDemo lockDemo1 = new DeadLockDemo(a, b);
            lockDemo1.dead(); // 锁住a和b
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(()->{
            DeadLockDemo lockDemo2 = new DeadLockDemo(b, c);
            lockDemo2.dead(); // 锁住a和b
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(()->{
            DeadLockDemo lockDemo3 = new DeadLockDemo(c, a);
            lockDemo3.dead(); // 锁住a和b
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }


    public static void main(String[] args) {
//        run(); // 顺序执行加锁,解锁
        rushRun(); // 线程进行抢
    }
}

数据一致性改进-Lua脚本

  • 锁: 多个线程抢—->每个线程按顺序完成
  • Lua: 原子性

在这里插入图片描述

采用单独lua脚本+配置类实现

在这里插入图片描述

创建Lua脚本

让减库存这个操作整体是原子性的

local key = KEYS[1]

--- 判断key是否存在
local isIn = tonumber(redis.call('exists',key))

if isIn==1 then
    local nums = tonumber(redis.call('get',key))
    if nums>0 then
        --- 减库存
        redis.call('decr',key)
        return redis.call('get',key)
    else
        return 'goods is sold out'
    end
else
    return 'goods not exists'
end


配置类

package com.tianju.redis.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

/**
 * lua脚本的Redis配置类
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisScript<String> redisScript(){
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(String.class); // 返回值类型
        redisScript.setLocation(new ClassPathResource("lua/goods.lua")); // 设置lua脚本的路径
        return redisScript;
    }
}

调用lua脚本

package com.tianju.redis.service.impl;

import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class RushGoodsServiceImpl implements IRushGoodsService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final String GOODS = "goods";

    @Autowired
    private RedisScript<String> redisScript;

    @Override
    public String rushLua(){
        return stringRedisTemplate.execute(
                redisScript,
                Collections.singletonList(GOODS)); // 创建只有一个元素的list
    }

}

在这里插入图片描述

将lua代码放入java中

package com.tianju.redis.service.impl;

import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class RushGoodsServiceImpl implements IRushGoodsService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final String GOODS = "goods";

    private final ReentrantLock lock = new ReentrantLock(); // 可重入锁
    @Override
    public String rush() {
        lock.lock();
        try{
            String sNum = stringRedisTemplate.opsForValue().get(GOODS);
            int nums = Integer.parseInt(sNum);
            if (nums>0){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stringRedisTemplate.opsForValue().decrement(GOODS); // 采用redis的原子减1
                return stringRedisTemplate.opsForValue().get(GOODS);
            }else {
                return "error";
            }
        }finally {
            lock.unlock();
        }
    }

    @Autowired
    private RedisScript<String> redisScript;

    @Override
    public String rushLua(){
        return stringRedisTemplate.execute(
                redisScript,
                Collections.singletonList(GOODS)); // 创建只有一个元素的list
    }

    @Override
    public String rushLuaJava() {
        String lua = "local key = KEYS[1]\n" +
                "\n" +
                "--- 判断key是否存在\n" +
                "local isIn = tonumber(redis.call('exists',key))\n" +
                "\n" +
                "if isIn==1 then\n" +
                "    local nums = tonumber(redis.call('get',key))\n" +
                "    if nums>0 then\n" +
                "        --- 减库存\n" +
                "        redis.call('decr',key)\n" +
                "        return redis.call('get',key)\n" +
                "    else\n" +
                "        return 'goods is sold out'\n" +
                "    end\n" +
                "else\n" +
                "    return 'goods not exists'\n" +
                "end";
       return stringRedisTemplate.opsForValue().getOperations()
                .execute(
                        new DefaultRedisScript<>(lua, String.class),
                        Collections.singletonList(GOODS)
                );
    }

}

分布式环境下

在这里插入图片描述

setnx:特点

如果key不存在,返回为1

如果key存在, 返回为0

在这里插入图片描述

package com.tianju.redis.service.impl;

import com.tianju.redis.service.IRushGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class RushGoodsServiceImpl implements IRushGoodsService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final String GOODS = "goods";

    private final String LOCK = "myLock";

    private final String DEFAULT = "1";



    private final ReentrantLock lock = new ReentrantLock(); // 可重入锁
    @Override
    public String rush() {
        lock.lock();
        try{
            String sNum = stringRedisTemplate.opsForValue().get(GOODS);
            int nums = Integer.parseInt(sNum);
            if (nums>0){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stringRedisTemplate.opsForValue().decrement(GOODS); // 采用redis的原子减1
                return stringRedisTemplate.opsForValue().get(GOODS);
            }else {
                return "error";
            }
        }finally {
            lock.unlock();
        }
    }

    @Autowired
    private RedisScript<String> redisScript;

    @Override
    public String rushLua(){
        return stringRedisTemplate.execute(
                redisScript,
                Collections.singletonList(GOODS)); // 创建只有一个元素的list
    }

    @Override
    public String rushLuaJava() {
        String lua = "local key = KEYS[1]\n" +
                "\n" +
                "--- 判断key是否存在\n" +
                "local isIn = tonumber(redis.call('exists',key))\n" +
                "\n" +
                "if isIn==1 then\n" +
                "    local nums = tonumber(redis.call('get',key))\n" +
                "    if nums>0 then\n" +
                "        --- 减库存\n" +
                "        redis.call('decr',key)\n" +
                "        return redis.call('get',key)\n" +
                "    else\n" +
                "        return 'goods is sold out'\n" +
                "    end\n" +
                "else\n" +
                "    return 'goods not exists'\n" +
                "end";
       return stringRedisTemplate.opsForValue().getOperations()
                .execute(
                        new DefaultRedisScript<>(lua, String.class),
                        Collections.singletonList(GOODS)
                );
    }

    @Override
    public String rushSetNx() {
        List<String> keys = new ArrayList<>();
        keys.add(GOODS);
        keys.add(LOCK);

        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, DEFAULT);
        

        System.out.println(flag);
        if (flag){ // 拿到锁
            return stringRedisTemplate.execute(
                    redisScript,
                    keys); // 创建只有一个元素的list
        }else {
            return "锁在使用中,请稍后";
        }
    }
}
local key = KEYS[1]
local myLock = KEYS[2]
--- 判断key是否存在
local isIn = tonumber(redis.call('exists',key))

if isIn==1 then
    local nums = tonumber(redis.call('get',key))
    if nums>0 then
        --- 减库存
        redis.call('decr',key)
        --- 删掉用于加锁的key,setnx
        redis.call('del',myLock)
        return redis.call('get',key)
    else
        return 'goods is sold out'
    end
else
    return 'goods not exists'
end


问题1: setnx 没有设置过期时间

问题2:执行的业务周期超过锁的有效时间

问题3: 业务没有执行完之前,锁要进行续期。

问题4: 可重入

Redisson框架

在分布环境下,redis的数据一致性解决方案.

https://redisson.org/

在这里插入图片描述

引入依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.22.0</version>
        </dependency>

配置类

package com.tianju.redis.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.130:6399");
        return Redisson.create(config);
    }
}

创建redis连接池

在这里插入图片描述

加锁和锁释放

在这里插入图片描述

package com.tianju.redis.service.impl;

import com.tianju.redis.service.IGoodsService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class GoodsServiceImpl implements IGoodsService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final String GOODS = "goods";

    private final String LOCK = "myLock";

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public String rushRedisson() {
        RLock lock = redissonClient.getLock(LOCK);
        lock.lock();// 默认的时间是30s
        try {
            String s = stringRedisTemplate.opsForValue().get(GOODS);
            int nums = Integer.parseInt(s);
            if (nums>0){
                // 库存-1操作
                stringRedisTemplate.opsForValue().decrement(GOODS);
                return stringRedisTemplate.opsForValue().get(GOODS);
            }else {
                return "销售完了";
            }
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}


总结

1.redis中数据一致性的问题,大量抢购如何保证数据安全;
2.用java代码加锁解决一致性问题,可重入锁,以及死锁的产生;
3.采用lua脚本,让库存-1操作原子化;
4.分布式环境下setnx锁,以及存在的问题;
5.Redisson框架的使用,锁的续期,看门狗策略,1/3时续期;

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/165079.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!