【分布式锁02 Redisson配置和使用】2大缺陷:主挂未同步,性能慢。双写不一致:延迟双删,内存队列,超时时间。读写锁源码。多级缓存:map redis 尝试上锁 在走缓存 读写锁。

  • 教程网址:

https://www.bilibili.com/video/BV1xA411G7wY?p=4&spm_id_from=pageDriver&vd_source=b63e9afd510deaf9d2a1b680368b9935

1. Redisson配置和使用

redis + son redis的儿子

  • 导入pom包
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
  • 配置
单机 集群 主从 复制 哨兵
@Configuration
public class RedissonConfig {

    @Bean
    public Redisson redisson() {
        Config config = new Config();

        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);

        return (Redisson) Redisson.create(config);
    }
}


        //集群
        config.useClusterServers();
        //哨兵
        config.useSentinelServers();
        //主从
        config.useMasterSlaveServers();
        //复制
        config.useReplicatedServers();
  • 使用
    @Autowired
    private Redisson redisson;
    
    //controller
	
	//封装了 如下
	//redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
    RLock lock = redisson.getLock(REDIS_LOCK);
        lock.lock();
        lock.unlock();

案例 RLock 和 Redisson

        RLock lock = redisson.getLock(REDIS_LOCK);
        lock.lock();
        
        try {
            String result = redis.opsForValue().get("goods:001");
            //如果为null,就为0,否则 转成 int
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);

            if (goodsNumber > 0) {
                //售卖了一个,还是这么多
                int realNumber = goodsNumber - 1;

                redis.opsForValue().set("goods:001", String.valueOf(realNumber));

                String r = "成功买到商品,还剩:" + realNumber + "\t 服务端口为:" + serverPort;
                System.out.println(r);

                return r;
            } else {
                System.out.println("意外情况,已经售空等" + serverPort);
            }
            return "fail";
        } finally {
            //如果是被锁定的
            if (lock.isLocked()) {
                //如果是 当前线程持有的话
                if (lock.isHeldByCurrentThread()) {
                    //才进行解锁
                    lock.unlock();
                }
            }
        }

2. 两大缺点:主未同步挂,排队性能差

  • 拿到 主节点的锁,主节点 还没同步到 子节点,就挂了。
  • 分布式锁,把 线程进行排队,性能差。

问题1 主节点挂了未同步

  1. 使用 zookeeper,AP的,强一致性,最低半数以上 都写成功了,才加锁成功
    1. zookeeper也是 key value,也能实现分布式锁。
    2. 是树形结构的目录。主节点叫 lead,从节点叫 follow
    3. C一致性,A可用性,P是分区容错性。
    4. lead挂了,一定是 同步成功key的节点,成为 新的 lead
  2. 如果能容忍,这种情况,用 redis
Redlock 解决 主节点挂了
  • 还没同步到 子节点

  • 对应 类为:RedissonLock

  • 超过半数redis节点加锁成功才算加锁成功

    • 对等的 3个 redis节点,半数以上 setNx加锁成功了,才是成功。
  • 旧版本是这样,使用 3个 客户端接口。

  • 新版本的构造为:public RedissonLock(CommandAsyncExecutor commandExecutor, String name)

        //创建配置
		Config config1 = new Config();
		//单机 服务端
        config1.useSingleServer().setAddress("redis://127.0.0.1:6379");
		//创建 RedissonClient
        RedissonClient redissonClient1 = Redisson.create(config1);
		//获取一个 锁
        RLock lock1 = redissonClient1.getLock("LOCK_KEY");
		
		//多个锁 组成 红锁 RedissonReadLock
        RedissonReadLock redLock = new RedissonReadLock(lock1, lock2, lock3);

        boolean isLock = false;
        try {
            //尝试 加锁
            isLock = redLock.tryLock(500, 1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            redLock.unlock();
        }

问题2 高并发和线程安全

  • ConcurrentHashMap 分段锁
  • 200 个产品,段位1 20个,段位2 20个,可以分10个段位
    • 分10段,并发 提升10倍
    • 段位 不够减去(一下买好多个),合并 其他段位。

3. Redis 缓存和数据库 双写不一致

案例1:值还未写入redis,值已经被改变

线程1:

写数据库stock=10
更新缓存stock=10,这里还未执行,被线程2 插队,插入完毕。

  • 更新缓存stock=10 开始执行,造成 不一致。

线程2:

写数据库stock=6
更新缓存stock=6

案例2:部分线程在 数据库更新后 才更新旧值

更新数据库,删除缓存。

查询数据库,加载缓存。

线程1:

  • 写数据库stock=10
    更新缓存stock=10

线程3:

  • 查缓存 空

  • 查数据库 stock = 10

  • 更新缓存 (被 线程2 抢先执行完毕),更新为 10

线程2:

  • 写数据库stock=20
  • 删除缓存

线程3:

  • 此时才更新缓存,更新为 10

解决

1. 延迟双删 无用

1、延迟双删:删除缓存后,睡X毫秒,再去删除

  • 如果 更新缓存 (被 线程2 抢先执行完毕),更新为 10。在这之后 执行,
  • 依然 无法解决
  • 并且 写的吞吐量 会降低。
2. 内存队列 无用

内存队列:所有对 redis的操作 放入 内存队列

  • 没用,降低 吞吐量
3. 加分布式锁
  • 加 分布式锁,
  • 写数据库stock=10 删除缓存,这样的操作 加锁。
配置 Redisson
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.5</version>
        </dependency>
@Configuration
public class RedissonConfig {

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}
3.1 加读写锁 RReadWriteLock
        String productId = "1";
        //使用 读写锁。
        RReadWriteLock readWriteLock = redisson.getReadWriteLock(REDIS_LOCK + productId);
        
		//读锁
		RLock rLock = readWriteLock.readLock();
        rLock.lock();
        rLock.unlock();//这是 读的情况。

        //写锁
        RLock wLock = readWriteLock.writeLock();
3.2 加DCL 双重锁
  • 是 JVM 加锁,部署多个项目的话,没用。
//检查缓存,如果存在,直接返回

synchronized(this){
	//先获取缓存,如果有 直接返回
	
	//查询数据
	
	//放入到缓存
}
4. 设置超时时间

设置 Redis 的超时时间

  • 商品 详情也 的库存,不是真实的 库存。

4. 分布式读写锁源码

  • 如果 都是 .readLock() 过来,第一 设置 setNx key,标志为 read模式,第二个会判断,是 read模式,

    • 当前是 读锁,就 +1
    • 锁释放 -1,知道 减到0 为止,锁删除。
  • 如果 是 readLock() 已经执行了,模式为 read 模式,

    • 一个 .writeLock() 过来,也会set 但是会失败,进入自旋。
  • 如果先执行 writeLock(),模式设置为了 write,其他的 读锁 和 写锁,也会进入 自旋。

读锁

源码
@Override
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(
            getName(), 
            LongCodec.INSTANCE, command,
            "local mode = redis.call('hget', KEYS[1], 'mode'); ",     
            Arrays.<Object>asList(getName(), 
            getReadWriteTimeoutNamePrefix(threadId)), 
            
            internalLockLeaseTime, 
            getLockName(threadId), 
            getWriteLockName(threadId));
    }
redis里键值对说明
redis的键为:atguiguLock101

key为:mode,value为:read

key为:fb7c6838-6a88-4117-a660-088493b67489:63,value为:1

redis的键为:{atguiguLock101}:fb7c6838-6a88-4117-a660-088493b67489:63:rwlock_timeout:1
redis的值为:1
lua脚本说明
					"local mode = redis.call('hget', KEYS[1], 'mode'); " +
                                "if (mode == false) then " +

					//keys[1]为前缀xxx+101 商品ID。如:atguiguLock101
					//使用hash 设置这个 redis键,hash的key为mode,value为 read
                     "redis.call('hset', KEYS[1], 'mode', 'read'); " +

					//进行加锁,加锁的 hash key为如下 ,value为1
					//fb7c6838-6a88-4117-a660-088493b67489:69:write
                    "redis.call('hset', KEYS[1], ARGV[2], 1); " +

					//设置这个:{atguiguLock101}:fb7c6838:63:rwlock_timeout:1 值为1
                                  "redis.call('set', KEYS[2] .. ':1', 1); " +
                                  "redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                                "end; " +

	//如果模式为 读锁, or mode为写锁,并且 这个hash 存在
	//如果为 读锁 或者 为同一个写锁
    "if (mode == 'read') or
(mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +

		//然后 增加1
        "local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " +

								  //这个redis 设置为 1
                                  "local key = KEYS[2] .. ':' .. ind;" +
                                  "redis.call('set', key, 1); " +
                                  "redis.call('pexpire', key, ARGV[1]); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);"

写锁的

@Override
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                            "local mode = redis.call('hget', KEYS[1], 'mode'); " +
                            "if (mode == false) then " +
                                  "redis.call('hset', KEYS[1], 'mode', 'write'); " +
                                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                                  "return nil; " +
                              "end; " +
                              "if (mode == 'write') then " +
                                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + 
                                      "local currentExpire = redis.call('pttl', KEYS[1]); " +
                                      "redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]); " +
                                      "return nil; " +
                                  "end; " +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);",
                        Arrays.<Object>asList(getName()), 
                        internalLockLeaseTime, getLockName(threadId));
    }

5. 多级缓存 和 只有查询:tryLock使用场景

  • 查询逻辑,第一次执行 放入redis,有了 缓存后,将 走不到 tryLock锁的逻辑里。
        //判断缓存是否存在
        RLock lock = redisson.getLock(REDIS_LOCK + productId);
		//lock.Lock() 如果不能容忍,偶尔大量的请求,打到数据库。

		//锁只等 3秒。抢锁失败,返回false,执行 下面的逻辑
        boolean b = lock.tryLock(3, TimeUnit.SECONDS);

		//再次判断

		//DCL 双重锁的逻辑
		//检查缓存,如果存在,直接返回

        synchronized(this){//换成 分布式锁
            //先获取缓存,如果有 直接返回

            //查询数据

            //放入到缓存
        }

查询的方法总览 多级缓存

从cache中 获取缓存
    private Product getProductFromCache(String productCacheKey) {
        Product product = null;

        //先去 map中 ,查询,多级 缓存 。内存能抗 更高的并发。因为 redis 有一次 远程的开销。
        Product mapProduct = productMap.get(productCacheKey);
        if (mapProduct != null) {
            return mapProduct;
        }

        String productStr = redis.opsForValue().get(productCacheKey);

        if (!StringUtils.isEmpty(productStr)) {
            if ("".equals(productCacheKey)) {
                product = new Product();
            }
            product = new Product();//productStr 转为 Product
        }
        return null;
    }
ID获产品:Map redis 获取锁 尝试上锁 在走缓存 读写锁
public Product get(Long productId) throws Exception {
        Product product = null;

        String productCacheKey = Product_cache + productId;
        //一般会在前面 + JVM 内存级别的 缓存。

        //先从缓存中 获取
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }

        RLock lock = redisson.getLock("LOCK_PRODUCT" + product);
        //允许 短时间 大量请求 进入,使用 tryLock 增加性能
        lock.tryLock(3, TimeUnit.SECONDS);

        try {
            //再次 从缓冲中 读取
            product = getProductFromCache(productCacheKey);
            if (product != null) {
                return product;
            }

            //获取 读写锁
            RReadWriteLock readWriteLock = redisson.getReadWriteLock("LOCK_PRODUCT" + product);
            RLock rLock = readWriteLock.readLock();
            rLock.lock();//这里使用 读锁,上锁

            try {
                //查询数据库
                product = new Product();
                if (product != null) {
                    //10分钟 超时,应该转为 json
                    redis.opsForValue().set(productCacheKey, product.toString(), 10, TimeUnit.MINUTES);

                    //map中也 放一份
                    productMap.put(productCacheKey, product);
                } else {
                    //为null,设置为空
                    redis.opsForValue().set(productCacheKey, new Product().toString(), 10, TimeUnit.MINUTES);
                }
            } finally {
                rLock.unlock();
            }
        } finally {
            lock.unlock();
        }
        return null;
    }

一线互联网 技术栈

分布式架构
微服务架构
源码分析
并发编程
高并发实战
性能优化
数据结构与算法
工程化协作
项目经验

Redis底层

字符串(String)

  • 整型(int)
    embstr编码的简单动态字符串
    raw编码的简单动态字符串

列表(List)

  • 双端链表(Linkedlist)
    压缩表(Ziplist)

哈希(Hash)

  • 哈希表(Hashtable)
    压缩表(Ziplist)

集合(Set)

  • 整型数组集合(lintset)
    哈希表(Hashtable)

有序集合(Zset)

  • 压缩表(ziplisi)
    跳跃表(Skiplist)

Bitmap 位图

GEO 地图

Hyperloglog是用来做基数统计的

其可以非常省内存的去统计各种计数,比如注册ip数、每日访问IP数、页面实时UV(PV肯定字符串就搞定了)、在线用户数

pfadd language "java" #添加成功
integer) 1
pfadd language "python"

pfcount language
integer) 2

pfadd language "java"  #再次添加失败
integer) 0
  • List item
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值