【软构件】实验5 Redisson

本文详细描述了使用Redisson技术解决缓存穿透问题,通过布隆过滤器与传统方法对比,以及在并发环境中测试其性能。同时探讨了Redisson与RabbitMQ在延迟队列应用上的差异,展示了分布式锁在库存管理中的改造。
摘要由CSDN通过智能技术生成

实验报告5

一、目的

实验目的:

掌握综合"中间件"Redisson的原理及编程模型

实验要求:

独立完成实验操作,并撰写实验报告

实验内容:

  1. Redisson布隆过滤器

使用布隆过滤器解决前述Redis章节的"缓存穿透"问题,并与之前的解决方案作对比。

说明:自定义数据表结构及数据;并发环境下对多种方案测试时,需要综合考虑测试数据及"缓存穿透"概率,并给出测试结果。

  1. Redisson延迟队列

模拟数据库功能(不必建立表,仅模拟即可),实现下单超过某一时间自动取消订单的功能。针对不同订单类型(如普通用户、VIP用户、企业用户),这个TTL时间也不相同。

尝试创建不同类型的订单(顺序尽可能完备测试),观察出队操作的顺序。

此外,对比RabbitMQ和Redisson的延迟队列,阐述它们之间的区别、各自的优缺点。

  1. Redisson分布式锁

使用Redisson分布式锁(类型不限),实现对实验4"进销存"库存的改造。

二、实验内容与设计思想

2.1 Redisson布隆过滤器
  • 缓存穿透问题:对于可能同一个不存在id一直未命中,所以一直在redis和数据库查询的问题。

  • 之前的解决方案:对于不存在id,如果数据库中不存在,将【id,空字符串】存入redis,输出NULL。这个方法只能解决每次都查询相同的不存在的id。如果是每次都查询不同的不存在的id,则不适用。依然存在冗余+穿透。

  • 使用布隆过滤器方案进行实验。

  • 并在并发环境下测试穿透概率。

2.2 Redisson延迟队列
  • 模拟普通用户、VIP用户、企业用户自动取消订单时间分别是10秒,20秒,30秒。(原则上应该是更长时间,如半小时,但是为了实验时间不太长,于是简化为秒级等待)

  • 建立不同类型的订单进行测试。

2.3 Redisson分布式锁
  • 使用redisson的分布式锁,对"进销存"库存的改造。

三、实验使用环境

平台:win10

软件:idea

四、实验步骤和调试过程

4.1 Redisson布隆过滤器

使用布隆过滤器解决前述Redis章节的"缓存穿透"问题,并与之前的解决方案作对比。

说明:自定义数据表结构及数据;并发环境下对多种方案测试时,需要综合考虑测试数据及"缓存穿透"概率,并给出测试结果。

4.1.1 代码

controller相关代码:

@RestController
@RequestMapping("/seckill")
public class SeckillController
{
    @Resource
    private SeckillService seckillService;
    @Autowired
    private RedissonDelayQueuePublisher delayQueuePublisher;
    //初始化,将数据库部分数据加载到redis
    @GetMapping("/init/{count}")
    public R initByCount(@PathVariable("count") int count)
    {
        return seckillService.initByCount(count);
    }

    //方式一查询,如果不存在则将(id,null)存入redis
    @GetMapping("/query1")
    public R query1(String id)
    {
        return seckillService.query1(id);
    }
    //方式二查询,布隆过滤器
    @GetMapping("/query2")
    public R query2(String id)
    {
        System.out.println("query2:"+id);
        return seckillService.query2(id);
    }

serviceImpl相关代码:

@Service("seckillService")
public class SeckillServiceImpl implements SeckillService
{
    private static int cnt = 0;
    @Resource
    private RedisService redisService;

    @Resource
    private RecordService recordService;

    @Resource
    private MessageService messageService;

    public static final String BLOOM_NAME = "bloomFilter";
    private RBloomFilter<String> bloomFilter;

    @Override
    public R initByCount(int count)
    {
        this.cnt = 0;
        //清空redis
        redisService.deleteValue();
        // 加载已有message到redis
        List<Message> records = (List<Message>) messageService.queryAll(null).getData();
        for (int i = 0; i < count && i < records.size(); i++)
        {
            Message record = records.get(i);
            redisService.addValue(record.getId() + "", record.getMessage());
        }
        //加载所有id到redisson
        bloomFilter = redissonClient.getBloomFilter(BLOOM_NAME);
        bloomFilter.tryInit(100000, 0.01); //HashKey空间大小,码重复率
        for (Message record : records)
        {
            System.out.println("===bloom:" + record.getId());
            bloomFilter.add(record.getId() + "");
        }
        //返回redis所有数据
        return redisService.getAllValue();
    }

    @Override
    public R query1(String id)
    {
        //1. 查询是否存在该id
        Object obj = redisService.getValue(id).getData();
        //2. 如果存在/不存在但已经查询过,则返回redis数据
        if (obj != null)
        {
            if (!"".equals("" + obj))
                return R.ok().setData(obj);
            else
                return R.error("不存在该id");
        }
        //3. 不存在查询数据库
        //3.1 加锁
        Boolean lock = (Boolean) redisService.reidsLock(id + "-lock", "lock").getData();
        if (lock)
        {
            //3.2 查询数据库,并写入redis。
            cnt++;
            System.out.println("穿透" + cnt + ":查询id:" + id);
            Message record = (Message) messageService.queryById(Integer.valueOf(id)).getData();
            //3.3 取消锁,返回数据
            if (record == null)
            {
                redisService.addValue(id, "");
                redisService.reidsUnlock(id + "-lock");
                return R.error("不存在该id");
            } else
            {
                redisService.addValue(id, record.getMessage());
                redisService.reidsUnlock(id + "-lock");
                return R.ok().setData(record.getMessage());
            }
        } else
        {
            System.out.println("查询失败");
            return R.error("查询失败");
        }
    }

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public R query2(String id)
    {
        //1. 查询是否存在该id
        Object obj = redisService.getValue(id).getData();
        //2. 如果存在,则返回redis数据
        if (obj != null && !"".equals("" + obj))
            return R.ok().setData(obj);
        //3. 不存在,布隆
        System.out.println("===" + bloomFilter);
        if (!bloomFilter.contains(id))
        {
            //3.1 布隆false,绝对不存在,返回null
            return R.error("不存在该id");
        } else
        {
            //3.2 布隆true,可能存在,查询数据库
            cnt++;
            System.out.println("穿透" + cnt + ":查询id:" + id);
            Message record = (Message) messageService.queryById(Integer.valueOf(id)).getData();
            if (record == null)
                return R.error("不存在该id");
            else
                return R.ok().setData(record.getMessage());
        }
    }
4.1.2 测试一

方案1:

首先进行初始化,清空redis中的键值,然后将数据库中1000个message的前100条保存到redis中,进行方案一的测试。

随机生成了1000个id,范围是1-2000内的整数,进行测试。

测试结果:

看出穿透数:670。

而redis中此时增加到了772个key。

方案2:

第一步,还是先进行初始化

使用同样的1000个测试数据进行测试,调用localhost:8081/seckill/query2进行id查询

结果如下:

穿透数:419,而redis中仍然是100个key。

总数据量1000(序号1-1000)
测试数据量1000(序号1-2000随机)
redis中数据量100(序号1-100)
布隆过滤器中数据量1000(序号1-1000)
方案1穿透670
方案2穿透419
4.1.3 测试二

按如上步骤,redis中数量增加到300,并更换测试数据,重新生成1000个数据。过程如下:

方案1穿透数674,redis有977个keys(穿透数反而比第一次更多,原因是因为第二次生成的随机数据,很多数据在300-2000中,所以造成了这个结果。本来想重新生成一批,还是保留了,也算是一次恶意数据较多的场景测试了。)

方案1穿透数333,redis中仍有100个keys。

测试结果如下:

总数据量1000(序号1-1000)
测试数据量1000(序号1-1000随机)
redis中数据量300(序号1-300)
布隆过滤器中数据量1000(序号1-1000)
方案1穿透677
方案2穿透333
4.1.4 测试三

按如上步骤,redis中数量增加到500,并更换测试数据,重新生成1000个数据(有了上一次的经验,我把数据控制在1-500中500个数据,501-2000中500个数据)。过程如下:

方案一穿透数408,有911个keys。

方案二穿透数181个,redis中有500个keys。

测试结果如下:

总数据量1000(序号1-1000)
测试数据量500(序号1-500随机)
500(序号501-2000随机)
redis中数据量500(序号1-500)
布隆过滤器中数据量1000(序号1-1000)
方案1穿透408
方案2穿透181
4.1.5 总结
总数据量1000(序号1-1000)
测试数据量1000(序号1-2000随机)
redis中数据量100(序号1-100)
布隆过滤器中数据量1000(序号1-1000)
方案1穿透670
方案2穿透419
总数据量1000(序号1-1000)
测试数据量1000(序号1-1000随机)
redis中数据量300(序号1-300)
布隆过滤器中数据量1000(序号1-1000)
方案1穿透677
方案2穿透333
总数据量1000(序号1-1000)
测试数据量500(序号1-500随机)500(序号501-2000随机)
redis中数据量500(序号1-500)
布隆过滤器中数据量1000(序号1-1000)
方案1穿透408
方案2穿透181
从上面三个测试可以看出,使用方案1,如果是每次都查询不同的不存在的id,效果并不好,依然存在冗余+穿透。(方案1适用于每次都查询相同的不存在的id。)

使用布隆过滤器则能够较好地解决,可以看到穿透率基本都是在方案1之下。

4.2 Redisson延迟队列

模拟数据库功能(不必建立表,仅模拟即可),实现下单超过某一时间自动取消订单的功能。针对不同订单类型(如普通用户、VIP用户、企业用户),这个TTL时间也不相同。

尝试创建不同类型的订单(顺序尽可能完备测试),观察出队操作的顺序。

此外,对比RabbitMQ和Redisson的延迟队列,阐述它们之间的区别、各自的优缺点。

4.2.1 代码

接收者:

@Component
@EnableScheduling
public class RedissonDelayQueueConsumer{
    //定义日志
    private static final Logger log= LoggerFactory.getLogger(RedissonDelayQueueConsumer.class);
    //定义Redisson的客户端操作实例
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 监听消费真正队列中的消息
     * 每时每刻都在不断的监听执行
     * @throws Exception
     */
    @Scheduled(cron = "*/1 * * * * ?")
    public void consumeMsg() throws Exception {
        //定义延迟队列的名称
        final String delayQueueName="redissonDelayQueueV3";
        RBlockingQueue<Message> rBlockingQueue=redissonClient.getBlockingQueue(delayQueueName);

        //从队列中弹出消息
        Message msg=rBlockingQueue.take();
        log.info("订单超时自动取消:{} ",msg);
        //TODO:在这里执行相应的业务逻辑
    }
}

发布者:

@Component
public class RedissonDelayQueuePublisher {
    //定义日志
    private static final Logger log= LoggerFactory.getLogger(RedissonDelayQueuePublisher.class);
    //定义Redisson的客户端操作实例
    @Autowired

    private RedissonClient redissonClient;

    /**
     * 发送消息入延迟队列
     * @param msg 消息
     * @param ttl 消息的存活时间-可以随意指定时间单位,在这里指毫秒
     */
    public void sendDelayMsg(final Message msg, final Long ttl){
        try {
            //定义延迟队列的名称
            final String delayQueueName="redissonDelayQueueV3";
            //定义获取阻塞式队列的实例
            RBlockingQueue<Message> rBlockingQueue=redissonClient.getBlockingQueue(delayQueueName);
            //定义获取延迟队列的实例
            RDelayedQueue<Message> rDelayedQueue=redissonClient.getDelayedQueue(rBlockingQueue);
            //往延迟队列发送消息-设置的TTL,相当于延迟了“阻塞队列”中消息的接收
            rDelayedQueue.offer(msg,ttl,TimeUnit.MILLISECONDS);
            log.info("订单记录publish:{}",msg);
        }catch (Exception e){
            log.error("Redisson延迟队列消息模型-生产者-发送消息入延迟队列-发生异常:{}",msg,e.fillInStackTrace());
        }
    }
}

controller中相关代码:

//发布延迟队列消息
@PostMapping
public R add(@RequestBody Message message)
{
    long time;
    if(message.getId()==1)
        time = 10000L; //普通
    else if(message.getId()==2)
        time = 20000L; //vip
    else
        time = 30000L; //企业
    delayQueuePublisher.sendDelayMsg(message, time);
    return R.ok().setData(message);
}
4.2.2 测试

根据不同的用户身份,创建三个订单,分别是10秒超时的普通用户订单,20秒超时的vip用户订单,30秒超时的企业用户订单(原则上应该是更长时间,如半小时,但是为了让实验等待时间不太长,于是简化为秒级等待)。

当同时发送,先后顺序为:普通用户->vip用户->企业用户,结果如下:可以看出超时顺序:普通用户->vip用户->企业用户。

再次测试,先后顺序更改为:企业用户->vip用户->普通用户,结果如下:可以看出超时顺序:普通用户->vip用户->企业用户。

由此,就可以看出RabbitMQ和Redisson的延迟队列的不同。

4.2.3 总结

RabbitMQ的队列虽然可以延迟,但是依然保留的队列的特性(即严格先进先出),不会自动扫描队列中元素的各自延迟时间,如果是上面的第二个测试,则超时顺序会是:企业用户->vip用户->普通用户。虽然普通用户时间到了,但是普通用户在队列中处于企业用户的后面,所以没有轮到它。

RabbitMQ优点就是性能比较高,因为不像Redisson,要扫描所有的元素判断时间是否超时。但是无法通过时间进行出列。

Redisson优点就是可以扫描所有元素进行超时判断,不受队列规则限制,缺点就是性能没有RabbitMQ高。

4.3 Redisson分布式锁

使用Redisson分布式锁(类型不限),实现对实验4"进销存"库存的改造。

4.3.1 代码

SeckillServiceImpl相关代码:

//加锁
public boolean acquire(String lockName, int waitTime)
{
    RLock mylock = redissonClient.getLock(lockName);
    mylock.lock(waitTime, TimeUnit.SECONDS);
    return true;
}

//释放锁
public void release(String lockName)
{
    RLock mylock = redissonClient.getLock(lockName);
    mylock.unlock();
}

@Autowired
private GoodsService goodsService;

@Override
public R updateRedisson(int id)
{
    // 0.通过id查询到goods
    Goods goods = (Goods) goodsService.queryById(id).getData();
    if (goods == null)
        return R.error("没有该id");
    // 1.加锁
    String lockName = goods.getGoodsId() + "-lock";
    while (!acquire(lockName, 3))
    {
        System.out.println("aaa");
    }
    // 2. 更新数据库
    goods = (Goods) goodsService.queryById(id).getData();
    if(goods.getInventory()<=0)
    {
        release(lockName);
        return R.error("库存不足");
    }
    goods.setInventory(goods.getInventory() - 1);
    goodsService.update(goods);
    recordService.insert(new Record(goods.getGoodsId(),null,null,null));
    System.out.println(goods.getGoodsId() + "库存-1");
    // 3.解锁
    release(lockName);
    return R.ok().setData(goodsService.queryById(goods.getGoodsId()));
}
4.3.2 测试

商品数据如下:

有10个商品,每个商品有10件。现在有0条记录。

创建一个测试,1秒发送1000个请求,分别对1000个商品进行抢购。

抢购结果如下:

可以看到100个商品刚好抢完,没有出现负库存的现象。并且记录刚好是100条,与商品数量相符。

五、实验小结

  1. 实验中遇到的问题及解决过程
  2. 实验中产生的错误及原因分析
  3. 实验体会和收获。

实验一小结:

使用方案1能够只能解决每次都查询相同的不存在的id。如果是每次都查询不同的不存在的id,效果并不好,依然存在冗余+穿透。

使用布隆过滤器则能够较好地解决。

实验二小结:

RabbitMQ的队列虽然可以延迟,但是依然保留的队列的特性(即严格先进先出),不会自动扫描队列中元素的各自延迟时间,如果是上面的第二个测试,则超时顺序会是:企业用户->vip用户->普通用户。虽然普通用户时间到了,但是普通用户在队列中处于企业用户的后面,所以没有轮到它。

RabbitMQ优点就是性能比较高,因为不像Redisson,要扫描所有的元素判断时间是否超时。但是无法通过时间进行出列。

Redisson优点就是可以扫描所有元素进行超时判断,不受队列规则限制,缺点就是性能没有RabbitMQ高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值