redis入门学习--实践篇-数据缓存-分布式锁-发布订阅-小白笔记

实践部分

实践代码来源:KnowledgePlanet / road-map / xfg-dev-tech-redis · GitCode

redisson 描述:1. 概述 · redisson/redisson Wiki · GitHub

  1. 可基于内存亦可持久化的日志型、Key-Value数据库,数据结构服务器因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

1. 数据缓存

分为数据插入、查询、更新等,Redis 的大部分操作其实都是缓存数据,提高系统的 QPS,在插入、更新、删除(逻辑删)、查询的时候,依赖于 Redis 进行提速操作。

  • 在插入数据的时候,可以一并切入缓存。如果有更新操作,可以考虑删除缓存,在查询更新。因为更新操作,很多时候都是部分字段更新,这个时候直接更新缓存容易不准。

  • 最后就是查询时,用缓存拦截,避免所有的查询都打到库上。这样可以提高系统的 QPS

  • 另外关于缓存击穿,说的就是你本来要在缓存存放大量数据的,但存放偏差或者漏了,那么这个时候大量请求都打到库上,导致把数据库拖垮。尤其是那种需要做事务加锁有资源竞争的,会更严重

  • 主要就是set get操作

  //在基础层的仓储处理中,也就是调用数据库DAO操作过程中同时实现redis查询和缓存
       @Override
          public OrderEntity queryOrder(String orderId) {
              OrderEntity orderEntity = redissonService.getValue(orderId);
              if (null == orderEntity) {
              UserOrderPO userOrderPO = userOrderDao.selectByOrderId(orderId);
                  orderEntity = OrderEntity.builder()
                              .userName(userOrderPO.getUserName())
                              .userId(userOrderPO.getUserId())
                              .userMobile(userOrderPO.getUserMobile())
                              .sku(userOrderPO.getSku())
                              .skuName(userOrderPO.getSkuName())
                              .orderId(userOrderPO.getOrderId())
                              .quantity(userOrderPO.getQuantity())
                              .unitPrice(userOrderPO.getUnitPrice())
                              .discountAmount(userOrderPO.getDiscountAmount())
                              .tax(userOrderPO.getTax())
                              .totalAmount(userOrderPO.getTotalAmount())
                              .orderDate(userOrderPO.getOrderDate())
                              .orderStatus(userOrderPO.getOrderStatus())
                              .uuid(userOrderPO.getUuid())
                              .build();
      
                  orderEntity.setDeviceVO(JSON.parseObject(userOrderPO.getExtData(), DeviceVO.class));
                  // 设置到缓存
                  redissonService.setValue(orderId, orderEntity);
              }
              return orderEntity;
          }

2. 加锁处理

使用 Redis 加分布式锁,也是分布式架构设计中非常常用的手段。常用于的场景包括;流程较长,耗时较多的个人开户、下单行为。也包括;一些资源竞争时加分布式锁,排队处理请求。

但对于资源竞争的这类库存占用,如果加分布式锁是非常影响系统的吞吐量的,因为所有的用户都在等待上一个用户做完流程后释放锁的处理,相当于你即使系统是分布式的了,但这里的分布式锁依然会把性能拖慢。所以如图,我们要考虑两种场景不同的加锁方式

  • 对于第1类的场景,主要是为了避免用户在一次操作后,又反复申请。系统上避免重复受理,所以添加分布式锁(独占锁)的方式进行拦截。如果不加分布式锁,就会进入到库表中通过唯一的索引拦截,这样对数据库的压力就比较大。

  • 对于第2类的场景,是采用了分段或者自增滑块的锁方式进行处理,减少对同一个锁的等待,而是生成一堆的锁,让用户去使用。也就是最开始案例背景的图中,一个个⭕️圆圈的分段锁

2.1 分布式锁

redis 分布式锁的 5个坑,真是又大又深 - YoungDeng - 博客园

Redis分布式锁—Redisson+RLock可重入锁实现篇 - niceyoo - 博客园

Java Jedis操作Redis示例(三)——setnx/getset实现分布式锁_java setnx-CSDN博客

解决分布式场景下的数据一致性问题;分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

乐观锁和悲观锁

  • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁

针对分布式锁的实现,目前比较常用的有以下几种方案:

  1. 基于数据库实现分布式锁

  2. 基于缓存(redis,memcached,tair)实现分布式锁

  3. 基于Zookeeper实现分布式锁

2.2 redis相关加锁实现

setNX + Lua脚本

Redis分布式锁—SETNX+Lua脚本实现篇 - niceyoo - 博客园
SET if Not eXists 的简写 setnx
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
必选参数说明:
SET:命令
key:待设置的key
value:设置的key的value,最好为随机字符串
可选参数说明:
NX:表示key不存在时才设置,如果存在则返回 null
XX:表示key存在时才设置,如果不存在则返回NULL
PX millseconds:设置过期时间,过期时间精确为毫秒
EX seconds:设置过期时间,过期时间精确为秒

Redis 2.6.12 版本之后,Redis 支持原子命令加锁,
我们可以通过向 Redis 发送 「set key value NX 过期时间」 命令,实现原子的加锁操作;
返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL key 来释放该锁。
如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。 

Redisson + RLock可重入锁

    Redisson 是 java 的 Redis 客户端之一,是 Redis 官网推荐的 java 语言实现分布式锁的项目。是在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。Redisson底层采用的是[Netty](http://netty.io/) 框架。支持[Redis](http://redis.cn) 2.8以上版本,支持Java1.6+以上版本。

    Redisson 提供了一些 api 方便操作 Redis。因为本文主要以锁为主,所以接下来我们主要关注锁相关的类,以下是 Redisson 中提供的多样化的锁:是目前大部分公司使用 Redis 分布式锁最常用的一种方式。
  • 可重入锁(Reentrant Lock 即RLock Java 对象)
  • 公平锁(Fair Lock)
  • 联锁(MultiLock)
  • 红锁(RedLock)
  • 读写锁(ReadWriteLock)
  • 信号量(Semaphore)
  • 闭锁(CountDownLatch)
  • 过期锁(Lease Lock)

pom.xml

  <dependency>
         <groupId>org.redisson</groupId>
         <artifactId>redisson-spring-boot-starter</artifactId>
          <version>3.23.4</version>
    </dependency>

redisson配置类

   /**
     * @author Fuzhengwei bugstack.cn @小傅哥
     * @description Redis 客户端,使用 Redisson <a href="https://github.com/redisson/redisson">Redisson</a>
     *rties(RedisClientConfigProperties是一个类一一对应于 
    @create 2023-09-09 16:51
     */
    @Configuration
    @EnableConfigurationProperties(RedisClientConfigProperties.class)
    public class RedisClientConfig {
    
        @Bean("redissonClient")
        public RedissonClient redissonClient(ConfigurableApplicationContext applicationContext, RedisClientConfigProperties properties) {
            Config config = new Config();
            // 根据需要可以设定编解码器;https://github.com/redisson/redisson/wiki/4.-%E6%95%B0%E6%8D%AE%E5%BA%8F%E5%88%97%E5%8C%96
            // config.setCodec(new RedisCodec());
    
            config.useSingleServer()
                    .setAddress("redis://" + properties.getHost() + ":" + properties.getPort())
                    .setPassword(properties.getPassword())
                    .setConnectionPoolSize(properties.getPoolSize())
                    .setConnectionMinimumIdleSize(properties.getMinIdleSize())
                    .setIdleConnectionTimeout(properties.getIdleTimeout())
                    .setConnectTimeout(properties.getConnectTimeout())
                    .setRetryAttempts(properties.getRetryAttempts())
                    .setRetryInterval(properties.getRetryInterval())
                    .setPingConnectionInterval(properties.getPingInterval())
                    .setKeepAlive(properties.isKeepAlive())
            ;
    
    
            RedissonClient redissonClient = Redisson.create(config);
    
            String[] beanNamesForType = applicationContext.getBeanNamesForType(MessageListener.class);
            for (String beanName : beanNamesForType) {
                MessageListener bean = applicationContext.getBean(beanName, MessageListener.class);
    
                Class<? extends MessageListener> beanClass = bean.getClass();
    
                if (beanClass.isAnnotationPresent(RedisTopic.class)) {
                    RedisTopic redisTopic = beanClass.getAnnotation(RedisTopic.class);
    
                    RTopic topic = redissonClient.getTopic(redisTopic.topic());
                    topic.addListener(String.class, bean);
    
                    ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
                    beanFactory.registerSingleton(redisTopic.topic(), topic);
                }
            }
    
            return redissonClient;
        }
        /**
     * 手动配置
     */
    @Bean("testRedisTopic")
    public RTopic testRedisTopicListener(RedissonClient redissonClient, RedisTopicListener01 redisTopicListener) {
        RTopic topic = redissonClient.getTopic("xfg-dev-tech-topic");
        topic.addListener(String.class, redisTopicListener);
        return topic;
    }

}

属性配置____

  
/**
 * @author Fuzhengwei bugstack.cn @小傅哥
 * @description Redis 连接配置
 * <a href="https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter">redisson-spring-boot-starter</a>
 * 对应于application.yml中的关于redis的配置,
 *
 * @create 2023-09-03 16:51
 */
@Data
@ConfigurationProperties(prefix = "redis.sdk.config", ignoreInvalidFields = true)
public class RedisClientConfigProperties {

    /** host:ip */
    private String host;
    /** 端口 */
    private int port;
    /** 账密 */
    private String password;
    /** 设置连接池的大小,默认为64 */
    private int poolSize = 64;
    /** 设置连接池的最小空闲连接数,默认为10 */
    private int minIdleSize = 10;
    /** 设置连接的最大空闲时间(单位:毫秒),超过该时间的空闲连接将被关闭,默认为10000 */
    private int idleTimeout = 10000;
    /** 设置连接超时时间(单位:毫秒),默认为10000 */
    private int connectTimeout = 10000;
    /** 设置连接重试次数,默认为3 */
    private int retryAttempts = 3;
    /** 设置连接重试的间隔时间(单位:毫秒),默认为1000 */
    private int retryInterval = 1000;
    /** 设置定期检查连接是否可用的时间间隔(单位:毫秒),默认为0,表示不进行定期检查 */
    private int pingInterval = 0;
    /** 设置是否保持长连接,默认为true */
    private boolean keepAlive = true;

}
RLock可重入锁指同一个线程可以多次获得同一个锁,而不会发生死锁。
 基于 RLock 接口,而 RLock 锁接口实现源码主要是 RedissonLock 这个类,而源码中加锁、释放锁等操作都是使用 Lua 脚本来完成的,并且封装的非常完善,开箱即用.
  • 若负责存储分布式锁的Redisson对象宕机且这个锁处于锁住状态就会出现锁死状态,为避免这个情况,redisson提供了监控锁的看门狗watchDog(在Redisson实例被关闭前,不断的延长锁的有效期,默认30秒)
   RLock lock = redisson.getLock("anyLock");
    // 最常见的使用方法
    lock.lock();     
  • 还可以利用参数来指定加锁时间:
  // 加锁以后10秒钟自动解锁
    // 无需调用unlock方法手动解锁
    lock.lock(10, TimeUnit.SECONDS);
    // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
    boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
    if (res) {
       try {
         ...
       } finally {
           lock.unlock();
       }
    }
  • 还有异步执行的方法:
  RLock lock = redisson.getLock("anyLock");
    lock.lockAsync();
    lock.lockAsync(10, TimeUnit.SECONDS);
    Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);

说明: RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.

分布式锁原理源码追溯:https://segmentfault.com/a/1190000041495173

以上代码都是Rlock 先通过getLock(lock_name_key)获取实现可重入分布式锁的类,
lock.lock() 加锁,尝试加锁,未加锁成功的线程订阅Redis的消息, 未加锁成功的线程通过自旋获取锁
lock.unlock()解锁,释放锁,取消到期续订;

底层是lua脚本实现
在这里插入图片描述


redisson基于了redis做的一个分布式锁,使用了类似redis的set key value nx命令的脚本,做的一个原子性建锁操作:如果锁不存在,则设置锁,并返回1(Long类型);如果锁存在,这返回0,锁存在,就代表着,有线程获取到了锁,并正在执行任务,其他的线程,会进入阻塞状态,在外部等待(独占式锁就是利用了这点)。在这里插入图片描述

 // 无锁化或者分段锁或者非独占式锁,
    // 核心在于处理有资源竞争的业务,例如库存秒杀-所有的用户都需要竞争
    //逻辑在于 每个线程进入到逻辑都可以去先将库存减一,
    //而不是必须等前一个线程执行完业务再锁定库存
      public String createOrderByNoLock(OrderAggregate orderAggregate) {
            SKUEntity skuEntity = orderAggregate.getSkuEntity();
            // 以下是 模拟锁商品库存
    //        先对sku 关键字 decr 减去一个库存;
            long decrCount = redissonService.decr(skuEntity.getSku());
            if (decrCount < 0) return "已无库存[初始化的库存和使用库存,保持一致。orderService.initSkuCount(\"13811216\", 10000);]";
    //  设置  锁关键字  sku_当前减少一个后的库存数量  每个锁一定要加上关键字和数据下量_
            String lockKey = skuEntity.getSku().concat("_").concat(String.valueOf(decrCount));
    //      RLock:redis锁对象 获得该锁
            RLock lock = redissonService.getLock(lockKey);
            try {
    //          加锁处理
                lock.lock();
                return createOrder(orderAggregate);
            } finally {
                lock.unlock();
            }
        }
   //独占式锁 意味着所有的业务逻辑必须要等着前一个线程都执行完才能执行
    //
      public String createOrderByLock(OrderAggregate orderAggregate) {
            RLock lock = redissonService.getLock("create_order_lock_".concat(orderAggregate.getSkuEntity().getSku()));
            try {
                lock.lock();//谁先到这里,谁先锁住,其他判断被锁了,只能不断自旋尝试获取锁
                long decrCount = redissonService.decr(orderAggregate.getSkuEntity().getSku());//锁了以后再执行业务逻辑;
                if (decrCount < 0) return "已无库存[初始化的库存和使用库存,保持一致。orderService.initSkuCount(\"13811216\", 10000);]";
                return createOrder(orderAggregate);
            } finally {
                lock.unlock();
            }
        }

3. 发布订阅

6. 分布式对象 - 6.7. 话题(订阅分发) - 《Redisson 使用手册》 - 书栈网 · BookStack

ps: 消息队列—— rabbitmq等消息中间件,而少用redis的发布订阅。

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。

方式一:subscri + publish

gif 演示如下:

  • 开启本地 Redis 服务,开启两个 redis-cli 客户端。

  • 第一个 redis-cli 客户端输入 SUBSCRIBE runoobChat,意思是订阅 runoobChat 频道。

  • 第二个 redis-cli 客户端输入 PUBLISH runoobChat “Redis PUBLISH test” 往 runoobChat 频道发送消息,这个时候在第一个 redis-cli 客户端就会看到由第二个 redis-cli 客户端发送的测试消息。
    在这里插入图片描述
    优点:支持多端订阅、简单、性能高
    缺点:数据会丢失

//blpop + lpush

   //程序1:使用代码实现订阅端
    while (running) {
    	try {
    		var msg = RedisHelper.BLPop(5, "list1");
    		if (string.IsNullOrEmpty(msg) == false) {
    			Console.WriteLine(msg);
    		}
    	} catch (Exception ex) {
    		Console.WriteLine(ex.Message);
    	}
    }
  //程序2:使用代码实现发布端
    RedisHelper.LPush("list1", "111");
  • 优势:数据不会丢失、简单、性能高;缺点:不支持多端(存在资源争抢);总结:为了解决方法一的痛点,我们实现了本方法,并且很漂亮的制造了一个新问题(不支持多端订阅)。

Java boot + redis 发布订阅

    redisson 提供了一种简易实现的订阅发布,适用于访问量不大的情况,否则会使内存负担较重。

Redisson的分布式话题RTopic

简单实现代码:

  // 创建Redisson客户端
    RedissonClient redisson = Redisson.create();
    // 获取RTopic对象
    RTopic<String> topic = redisson.getTopic("myTopic");
    // 发布消息
    topic.publish("Hello, Redisson!");
    // 添加监听器
    topic.addListener(String.class, (channel, msg) -> {
        System.out.println("Received message: " + msg);
    });
    // 关闭Redisson客户端
    redisson.shutdown();
  • 发布和订阅实现基于 Redis 的事件机制,即订阅者通过执行 SUBSCRIBE 命令将自己的监听器添加到 Redis 服务器维护的事件循环器中,当发布者通过 PUBLISH 命令向指定频道发送消息时,Redis 服务器会将消息发送给监听该频道的所有订阅者;
  • 在Redis节点故障转移(主从切换)或断线重连以后,所有的话题监听器将自动完成话题的重新订阅。
  • 发布订阅是我们需要对同一个 Topic 进行发布和监听操作。但这个操作的代码是一种手动编码。实际开发中,改变手动编码模式,通过自定义注解,来完成动态监听和将对象动态注入到 Spring 容器中,让需要注入的属性,可以被动态注入.
  • 3.1.1 自定义注解
package cn.bugstack.xfg.dev.tech.types;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface RedisTopic {
    String topic() default "";
}
  • 3.1.2 注解使用
@Slf4j
@Service
@RedisTopic(topic = "testRedisTopic02")
public class RedisTopicListener02 implements MessageListener<String> {

    @Override
    public void onMessage(CharSequence channel, String msg) {
        log.info("02-监听消息(Redis 发布/订阅): {}", msg);
    }
}
  • 3.1.3 动态注入
// 添加监听
String[] beanNamesForType = applicationContext.getBeanNamesForType(MessageListener.class);
for (String beanName : beanNamesForType) {
    MessageListener bean = applicationContext.getBean(beanName, MessageListener.class);
    Class<?> beanClass = bean.getClass();
    if (beanClass.isAnnotationPresent(RedisTopic.class)) {
        RedisTopic redisTopic = beanClass.getAnnotation(RedisTopic.class);
        RTopic topic = redissonClient.getTopic(redisTopic.topic());
        topic.addListener(String.class, bean);
        
        // 动态创建 bean 对象,注入到 spring 容器,bean 的名称为 redisTopic,对象为 RTopic
        ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        beanFactory.registerSingleton(redisTopic.topic(), topic);
    }
}
  • 3.1.4 使用
 		@Resource(name = "testRedisTopic02")
    private RTopic testRedisTopic02;
    @Resource(name = "testRedisTopic03")
    private RTopic testRedisTopic03;
    @Override
    public String createOrder(OrderAggregate orderAggregate) {
        // 省略...
        testRedisTopic02.publish(JSON.toJSONString(orderEntity));
        testRedisTopic03.publish(JSON.toJSONString(orderEntity));
        return orderId;
    }    
  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值