实验报告5
一、目的
实验目的:
掌握综合"中间件"Redisson的原理及编程模型
实验要求:
独立完成实验操作,并撰写实验报告
实验内容:
- Redisson布隆过滤器
使用布隆过滤器解决前述Redis章节的"缓存穿透"问题,并与之前的解决方案作对比。
说明:自定义数据表结构及数据;并发环境下对多种方案测试时,需要综合考虑测试数据及"缓存穿透"概率,并给出测试结果。
- Redisson延迟队列
模拟数据库功能(不必建立表,仅模拟即可),实现下单超过某一时间自动取消订单的功能。针对不同订单类型(如普通用户、VIP用户、企业用户),这个TTL时间也不相同。
尝试创建不同类型的订单(顺序尽可能完备测试),观察出队操作的顺序。
此外,对比RabbitMQ和Redisson的延迟队列,阐述它们之间的区别、各自的优缺点。
- 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能够只能解决每次都查询相同的不存在的id。如果是每次都查询不同的不存在的id,效果并不好,依然存在冗余+穿透。
使用布隆过滤器则能够较好地解决。
实验二小结:
RabbitMQ的队列虽然可以延迟,但是依然保留的队列的特性(即严格先进先出),不会自动扫描队列中元素的各自延迟时间,如果是上面的第二个测试,则超时顺序会是:企业用户->vip用户->普通用户。虽然普通用户时间到了,但是普通用户在队列中处于企业用户的后面,所以没有轮到它。
RabbitMQ优点就是性能比较高,因为不像Redisson,要扫描所有的元素判断时间是否超时。但是无法通过时间进行出列。
Redisson优点就是可以扫描所有元素进行超时判断,不受队列规则限制,缺点就是性能没有RabbitMQ高。