Redis 缓存、加锁(独占/分段)、发布/订阅,常用特性的使用和高级编码操作

前言:

Redis 的基本缓存使用、Redis 加锁(Redisson 提供了很多锁的方式,这里我们会展示独占锁和无锁化的性能测试)。之后还有一个非常重要的场景是关于 Redis 的发布和订阅。
功能实现

代码目录结构


工程分为,app、domain、infrastructure、trigger 这样的四层,其实还有一个 types 通用层。

1:app;用于配置 Redis 的相关启动操作,鉴于 SpringBoot 以及 Redis 版本问题,这里我们自己来创建客户端,更好兼容版本的差异。同时也可以扩展一些额外的功能。

2:domain;是领域服务层,order 可以看做是一个订单域,包括订单的创建、支付、查询,都可以在这个领域实现。这个订单领域涉及的表就是前面章节,所压测的表 【压测】MySQL 连接池 c3p0、dbcp、druid、hikari(opens new window)

3:infrastructure;基础层是对 domain 依赖倒置的实现,具体到库的操作、缓存的操作,都是用这一层来实现。所以我们操作 Redis 的加锁、缓存,也会放到这里来处理。

4:trigger;触发器层,一般也有叫接口层。一般 http、rpc、job、mq、listener 都是在这一层进来使用。所以我们订阅 Redis 的消息也是放到这一层中处理。

5:types;工程中还有一个通用类型层,定义一些非专属 domain 领域内的公共资源。如配置一个自定义注解,来处理一些类的动态加载和组件开发。本章中我们就定义了一个这样的注解,来动态注入实例化的 Bean 对象。这块非常值得学习一下,因为它是解决此类场景的高级编码。
数据缓存

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

设置代码

java 复制代码
 // 设置到缓存
        redissonService.setValue(orderId, orderEntity);

        testRedisTopic.publish(JSON.toJSONString(orderEntity));

        testRedisTopic02.publish(JSON.toJSONString(orderEntity));
        testRedisTopic03.publish(JSON.toJSONString(orderEntity));

        return orderId;
    }

    @Override
    public OrderEntity queryOrder(String orderId) {
        OrderEntity orderEntity = redissonService.getValue(orderId);
        if (null == orderEntity) {
            UserOrderPO userOrderPO = userOrderDao.selectByOrderId(orderId);
            orderEntity = new OrderEntity();
            orderEntity.setUserName(userOrderPO.getUserName());
            orderEntity.setUserId(userOrderPO.getUserId());
            orderEntity.setUserMobile(userOrderPO.getUserMobile());
            orderEntity.setSku(userOrderPO.getSku());
            orderEntity.setSkuName(userOrderPO.getSkuName());
            orderEntity.setOrderId(userOrderPO.getOrderId());
            orderEntity.setQuantity(userOrderPO.getQuantity());
            orderEntity.setUnitPrice(userOrderPO.getUnitPrice());
            orderEntity.setDiscountAmount(userOrderPO.getDiscountAmount());
            orderEntity.setTax(userOrderPO.getTax());
            orderEntity.setTotalAmount(userOrderPO.getTotalAmount());
            orderEntity.setOrderDate(userOrderPO.getOrderDate());
            orderEntity.setOrderStatus(userOrderPO.getOrderStatus());
            orderEntity.setUuid(userOrderPO.getUuid());
            orderEntity.setDeviceVO(JSON.parseObject(userOrderPO.getExtData(), DeviceVO.class));
            // 设置到缓存
            redissonService.setValue(orderId, orderEntity);
        }
        return orderEntity;
    }

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

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

使用 Redis 加分布式锁,也是分布式架构设计中非常常用的手段。常用于的场景包括;流程较长,耗时较多的个人开户、下单行为。也包括;一些资源竞争时加分布式锁,排队处理请求。但对于资源竞争的这类库存占用,如果加分布式锁是非常影响系统的吞吐量的,因为所有的用户都在等待上一个用户做完流程后释放锁的处理,相当于你即使系统是分布式的了,但这里的分布式锁依然会把性能拖慢。

独占锁模式

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

java 复制代码
   @Override
    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();
        }
    }

分段锁模式

java 复制代码
 @Override
    public String createOrderByNoLock(OrderAggregate orderAggregate) {
        SKUEntity skuEntity = orderAggregate.getSkuEntity();

        // 模拟锁商品库存
        long decrCount = redissonService.decr(skuEntity.getSku());
        if (decrCount < 0) {
            return "已无库存[初始化的库存和使用库存,保持一致。orderService.initSkuCount(\"13811216\", 10000);]";
        }
        String lockKey = skuEntity.getSku().concat("_").concat(String.valueOf(decrCount));

        RLock lock = redissonService.getLock(lockKey);

        try {
            lock.lock();
            return createOrder(orderAggregate);
        } finally {
            lock.unlock();
        }
    }

分段或者自增滑块的锁方式进行处理,减少对同一个锁的等待,而是生成一堆的锁,让用户去使用。
发布/订阅

会涉及到如何向 Spring 动态注入已经实例化后的 Bean 对象。为什么会出现这个场景呢?

java 复制代码
// 创建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();

发布和订阅,是我们需要对同一个 Topic 进行发布和监听操作。但这个操作的代码是一种手动编码,但在我们实际使用中,如果所有的都是手动编码,一个是非常麻烦,再有一个是非常累人。

来个高级编码,通过自定义注解,来完成动态监听和将对象动态注入到 Spring 容器中,让需要注入的属性,可以被动态注入。

自定义注解

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface RedisTopic {

    String topic() default "";

}

注解使用

java 复制代码
@Slf4j
@Service
@RedisTopic(topic = "testRedisTopic02")
public class RedisTopicListener02 implements MessageListener<String> {

    @Override
    public void onMessage(CharSequence channel, String msg) {
        log.info("02-监听消息(Redis 发布/订阅): {}", msg);
    }

}

动态注入

java 复制代码
// 添加监听
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);
    }
}

使用对象

java 复制代码
@Slf4j
@Repository
public class OrderRepository implements IOrderRepository {

    @Resource
    private IRedisService redissonService;
    @Resource
    private IUserOrderDao userOrderDao;
    
    @Resource
    private RTopic testRedisTopic;

    @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;
    }    
}    

功能测试

java 复制代码
@Test
public void test_createOrder() throws InterruptedException {
    String sku = RandomStringUtils.randomNumeric(9);
    int count = 10000;
    orderService.initSkuCount(sku, count);
  
    for (int i = 0; i < count; i++) {
        threadPoolExecutor.execute(() -> {
            UserEntity userEntity = UserEntity.builder()
                    .userId("小傅哥")
                    .userName("xfg".concat(RandomStringUtils.randomNumeric(3)))
                    .userMobile("+86 13521408***")
                    .build();
            SKUEntity skuEntity = SKUEntity.builder()
                    .sku(sku)
                    .skuName("《手写MyBatis:渐进式源码实践》")
                    .quantity(1)
                    .unitPrice(BigDecimal.valueOf(128))
                    .discountAmount(BigDecimal.valueOf(50))
                    .tax(BigDecimal.ZERO)
                    .totalAmount(BigDecimal.valueOf(78))
                    .build();
            DeviceVO deviceVO = DeviceVO.builder()
                    .ipv4("127.0.0.1")
                    .ipv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334".getBytes())
                    .machine("IPhone 14 Pro")
                    .location("shanghai")
                    .build();
            long threadBeginTime = System.currentTimeMillis(); // 记录线程开始时间
            // 耗时:4毫秒
               String orderId = orderService.createOrder(new OrderAggregate(userEntity, skuEntity, deviceVO));
            // 耗时:106毫秒
              String orderId = orderService.createOrderByLock(new OrderAggregate(userEntity, skuEntity, deviceVO));
            // 耗时:4毫秒
            String orderId = orderService.createOrderByNoLock(new OrderAggregate(userEntity, skuEntity, deviceVO));
            long took = System.currentTimeMillis() - threadBeginTime;
            totalExecutionTime.addAndGet(took); // 累加线程耗时
            log.info("写入完成 {} 耗时 {} (ms)", orderId, took / 1000);
        });
    }
    new Thread(() -> {
        while (true) {
            if (threadPoolExecutor.getActiveCount() == 0) {
                log.info("执行完毕,总耗时:{} (ms)", (totalExecutionTime.get() / 1000));
                  log.info("执行完毕,总耗时:{}", "\r\033[31m" + (totalExecutionTime.get() / 1000) + "\033[0m");
                break;
            }
            try {
                Thread.sleep(350);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
    // 等待
    new CountDownLatch(1).await();
}

测试前,记得修改代码 count 值,代表这要初始化内存多少个容量。另外是环境记得先执行安装。

接下来,我们进入了压测环节。createOrder 不使用锁、createOrderByLock 使用独占锁、createOrderByNoLock 是分段锁,也可以当做无锁处理。
其他测试

读写锁、异步锁、信号量、队列、延迟队列的相关测试。

java 复制代码
/**
 * 延迟队列场景应用;https://mp.weixin.qq.com/s/jJ0vxdeKXHiYZLrwDEBOcQ
 */
@Test
public void test_getDelayedQueue() throws InterruptedException {
    RBlockingQueue<Object> blockingQueue = redissonService.getBlockingQueue("xfg-dev-tech-task");
    RDelayedQueue<Object> delayedQueue = redissonService.getDelayedQueue(blockingQueue);
    new Thread(() -> {
        try {
            while (true){
                Object take = blockingQueue.take();
                log.info("测试结果 {}", take);
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    int i = 0;
    while (true){
        delayedQueue.offerAsync("测试" + ++i, 100L, TimeUnit.MILLISECONDS);
        Thread.sleep(1000L);
    }
}

好了 至此 Redis 缓存、加锁(独占/分段)、发布/订阅,常用特性的使用和高级编码操作 学习结束了 友友们 点点关注不迷路 老铁们!!!!!

相关推荐
月光水岸New2 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6752 小时前
数据库基础1
数据库
我爱松子鱼2 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo2 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser3 小时前
【SQL】多表查询案例
数据库·sql
Galeoto3 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
希忘auto4 小时前
详解Redis在Centos上的安装
redis·centos
人间打气筒(Ada)4 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231114 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql
喝醉酒的小白4 小时前
PostgreSQL:更新字段慢
数据库·postgresql