learn.lianglianglee.com/专栏/分布式技术原理与实战45讲-完/41 失效策略:缓存过期都有哪些策略?.md.html
2022-05-11 18:57:05 +08:00

1457 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>41 失效策略:缓存过期都有哪些策略?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/00 开篇词:搭建分布式知识体系,挑战高薪 Offer.md.html">00 开篇词:搭建分布式知识体系,挑战高薪 Offer.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/01 如何证明分布式系统的 CAP 理论?.md.html">01 如何证明分布式系统的 CAP 理论?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/02 不同数据一致性模型有哪些应用?.md.html">02 不同数据一致性模型有哪些应用?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/03 如何透彻理解 Paxos 算法?.md.html">03 如何透彻理解 Paxos 算法?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/04 ZooKeeper 如何保证数据一致性?.md.html">04 ZooKeeper 如何保证数据一致性?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/05 共识问题:区块链如何确认记账权?.md.html">05 共识问题:区块链如何确认记账权?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/06 如何准备一线互联网公司面试?.md.html">06 如何准备一线互联网公司面试?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/07 分布式事务有哪些解决方案?.md.html">07 分布式事务有哪些解决方案?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/08 对比两阶段提交,三阶段协议有哪些改进?.md.html">08 对比两阶段提交,三阶段协议有哪些改进?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/09 MySQL 数据库如何实现 XA 规范?.md.html">09 MySQL 数据库如何实现 XA 规范?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/10 如何在业务中体现 TCC 事务模型?.md.html">10 如何在业务中体现 TCC 事务模型?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/11 分布式锁有哪些应用场景和实现?.md.html">11 分布式锁有哪些应用场景和实现?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/12 如何使用 Redis 快速实现分布式锁?.md.html">12 如何使用 Redis 快速实现分布式锁?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/13 分布式事务考点梳理 + 高频面试题.md.html">13 分布式事务考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/14 如何理解 RPC 远程服务调用?.md.html">14 如何理解 RPC 远程服务调用?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/15 为什么微服务需要 API 网关?.md.html">15 为什么微服务需要 API 网关?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/16 如何实现服务注册与发现?.md.html">16 如何实现服务注册与发现?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/17 如何实现分布式调用跟踪?.md.html">17 如何实现分布式调用跟踪?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/18 分布式下如何实现配置管理?.md.html">18 分布式下如何实现配置管理?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/19 容器化升级对服务有哪些影响?.md.html">19 容器化升级对服务有哪些影响?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/20 ServiceMesh服务网格有哪些应用.md.html">20 ServiceMesh服务网格有哪些应用.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/21 Dubbo vs Spring Cloud两大技术栈如何选型.md.html">21 Dubbo vs Spring Cloud两大技术栈如何选型.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/22 分布式服务考点梳理 + 高频面试题.md.html">22 分布式服务考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/23 读写分离如何在业务中落地?.md.html">23 读写分离如何在业务中落地?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/24 为什么需要分库分表,如何实现?.md.html">24 为什么需要分库分表,如何实现?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/25 存储拆分后,如何解决唯一主键问题?.md.html">25 存储拆分后,如何解决唯一主键问题?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/26 分库分表以后,如何实现扩容?.md.html">26 分库分表以后,如何实现扩容?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/27 NoSQL 数据库有哪些典型应用?.md.html">27 NoSQL 数据库有哪些典型应用?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/28 ElasticSearch 是如何建立索引的?.md.html">28 ElasticSearch 是如何建立索引的?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/29 分布式存储考点梳理 + 高频面试题.md.html">29 分布式存储考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/30 消息队列有哪些应用场景?.md.html">30 消息队列有哪些应用场景?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/31 集群消费和广播消费有什么区别?.md.html">31 集群消费和广播消费有什么区别?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/32 业务上需要顺序消费,怎么保证时序性?.md.html">32 业务上需要顺序消费,怎么保证时序性?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/33 消息幂等:如何保证消息不被重复消费?.md.html">33 消息幂等:如何保证消息不被重复消费?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/34 高可用:如何实现消息队列的 HA.md.html">34 高可用:如何实现消息队列的 HA.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/35 消息队列选型Kafka 如何实现高性能?.md.html">35 消息队列选型Kafka 如何实现高性能?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/36 消息队列选型RocketMQ 适用哪些场景?.md.html">36 消息队列选型RocketMQ 适用哪些场景?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/37 消息队列考点梳理 + 高频面试题.md.html">37 消息队列考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/38 不止业务缓存,分布式系统中还有哪些缓存?.md.html">38 不止业务缓存,分布式系统中还有哪些缓存?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/39 如何避免缓存穿透、缓存击穿、缓存雪崩?.md.html">39 如何避免缓存穿透、缓存击穿、缓存雪崩?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/40 经典问题:先更新数据库,还是先更新缓存?.md.html">40 经典问题:先更新数据库,还是先更新缓存?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/分布式技术原理与实战45讲-完/41 失效策略:缓存过期都有哪些策略?.md.html">41 失效策略:缓存过期都有哪些策略?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/42 负载均衡:一致性哈希解决了哪些问题?.md.html">42 负载均衡:一致性哈希解决了哪些问题?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/43 缓存高可用:缓存如何保证高可用?.md.html">43 缓存高可用:缓存如何保证高可用?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/44 分布式缓存考点梳理 + 高频面试题.md.html">44 分布式缓存考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/45 从双十一看高可用的保障方式.md.html">45 从双十一看高可用的保障方式.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/46 高并发场景下如何实现系统限流?.md.html">46 高并发场景下如何实现系统限流?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/47 降级和熔断:如何增强服务稳定性?.md.html">47 降级和熔断:如何增强服务稳定性?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/48 如何选择适合业务的负载均衡策略?.md.html">48 如何选择适合业务的负载均衡策略?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/49 线上服务有哪些稳定性指标?.md.html">49 线上服务有哪些稳定性指标?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/50 分布式下有哪些好用的监控组件?.md.html">50 分布式下有哪些好用的监控组件?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/51 分布式下如何实现统一日志系统?.md.html">51 分布式下如何实现统一日志系统?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/52 分布式路漫漫,厚积薄发才是王道.md.html">52 分布式路漫漫,厚积薄发才是王道.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>41 失效策略:缓存过期都有哪些策略?</h1>
<p>缓存使用的是内存资源,而内存资源是非常宝贵的,要用有限的服务器资源支撑更多的业务,就必须让那些访问频率不高的缓存删除掉,为新的缓存腾出内存空间。这一课时,我们一起来看一下,缓存失效有哪些策略。</p>
<h3>页面置换算法</h3>
<p>我们从一开始就提到,缓存技术在计算机系统设计中有着非常广泛的应用,对应到操作系统中,就是缓存页面的调度算法。</p>
<p>在操作系统中,文件的读取会先分配一定的页面空间,也就是我们说的 Page使用页面的时候首先去查询空间是否有该页面的缓存如果有的话则直接拿出来否则就先查询页面空间没有满就把新页面缓存起来如果页面空间满了就删除部分页面方便新的页面插入。</p>
<p>在操作系统的页面空间中,对应淘汰旧页面的机制不同,所以会有不同页面调度方法,常见的有 FIFO、LRU、LFU 过期策略:</p>
<ul>
<li><strong>FIFOFirst In First Out先进先出</strong>,根据缓存被存储的时间,离当前最远的数据优先被淘汰;</li>
<li><strong>LRULeast Recently Used最近最少使用</strong>,根据最近被使用的时间,离当前最远的数据优先被淘汰;</li>
<li><strong>LFULeast Frequently Used最不经常使用</strong>,在一段时间内,缓存数据被使用次数最少的会被淘汰。</li>
</ul>
<p>这三种策略也是经典的缓存淘汰策略,大部分缓存应用模型,都是基于这几种策略实现的。</p>
<h3>内存淘汰策略</h3>
<p>操作系统的页面置换算法,对应到分布式缓存中,就是缓存的内存淘汰策略,这里以 Redis 为例,进行分析。</p>
<p>当 Redis 节点分配的内存使用到达最大值以后为了继续提供服务Redis 会启动内存淘汰策略,以下的几种策略参考官方文档:</p>
<ul>
<li>noeviction这是默认的策略对于写请求会拒绝服务直接返回错误这种策略下可以保证数据不丢失</li>
<li>allkeys-lru这种策略操作的范围是所有 key使用 LRU 算法进行缓存淘汰;</li>
<li>volatile-lru这种策略操作的范围是设置了过期时间的 key使用 LRU 算法进行淘汰;</li>
<li>allkeys-random这种策略下操作的范围是所有 key会进行随机淘汰数据</li>
<li>volatile-random这种策略操作的范围是设置了过期时间的 key会进行随机淘汰</li>
<li>volatile-ttl这种策略操作的范围是设置了过期时间的 key根据 key 的过期时间进行淘汰,越早过期的越优先被淘汰。</li>
</ul>
<h3>缓存过期策略</h3>
<p>分布式缓存中的过期策略和内存淘汰策略又有一些不同,希望大家不要混淆,内存淘汰是缓存服务层面的操作,而过期策略定义的是具体缓存数据何时失效,下面一起来看一下。</p>
<p>我们都知道Redis 是 key-value 数据库,可以设置缓存 key 的过期时间,过期策略就是指当 Redis 中缓存的 key 过期了Redis 如何处理。</p>
<p>Redis 中过期策略通常有以下三种。</p>
<ul>
<li>定时过期</li>
</ul>
<p>这是最常见也是应用最多的策略,为每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。这种方式可以立即删除过期数据,避免浪费内存,但是需要耗费大量的 CPU 资源去处理过期的数据,可能影响缓存服务的性能。</p>
<ul>
<li>惰性过期</li>
</ul>
<p>可以类比懒加载的策略,这个就是懒过期,只有当访问一个 key 时,才会判断该 key 是否已过期,并且进行删除操作。这种方式可以节省 CPU 资源,但是可能会出现很多无效数据占用内存,极端情况下,缓存中出现大量的过期 key 无法被删除。</p>
<ul>
<li>定期过期</li>
</ul>
<p>这种方式是上面方案的整合,添加一个即将过期的缓存字典,每隔一定的时间,会扫描一定数量的 key并清除其中已过期的 key。</p>
<p>合理的缓存配置需要协调内存淘汰策略和过期策略避免内存浪费同时最大化缓存集群的吞吐量。另外Redis 的缓存失效有一点特别关键,那就是如何避免大量主键在同一时间同时失效造成数据库压力过大的情况,对于这个问题在第 33 课时缓存穿透中有过描述,大家可以去扩展了解下。</p>
<h3>实现一个 LRU 缓存</h3>
<p>下面介绍一个高频的面试问题:如何实现一个 LRU 算法该算法的实现很多同学都听过但是不知道你还记不记得那句经典的格言Talk is cheapshow me the code。很多人在写代码时一说就懂一写就错特别在面试时常常要求你白板编程脱离了 IDE 的帮助,更容易出现错误,所以我建议大家动手去实现一下。</p>
<p>在 Java 语言中实现 LUR 缓存,可以直接应用内置的 LinkedHashMap重写对应的 removeEldestEntry() 方法,代码如下:</p>
<pre><code>public class LinkedHashMapExtend extends LinkedHashMap {
private int cacheSize;
public LinkedHashMapExtend(int cacheSize){
super();
this.cacheSize=cacheSize;
}
@Override
public boolean removeEldestEntry(Map.Entry eldest) {
//重写移除逻辑
if(size()&gt;cacheSize){
return true;
}
return false;
}
}
</code></pre>
<p>为什么重写 LinkedHashMap 可以实现 LRU 缓存呢?</p>
<p>对于这个问题,我建议你可以查看一下 LinkedHashMap 的源码实现,在原生的 removeEldestEntry 实现中,默认返回了 false也就是永远不会移除最“早”的缓存数据只要扩展这个条件缓存满了移除最早的数据是不是就实现了一个 LRU 策略?</p>
<p>在面试中,单纯使用 LinkedHashMap 实现是不够的,还会要求你使用原生的 Map 和双向链表来实现。下面我简单实现了一个参考代码,这道题目在 Leetcode 上的编号是 146也是剑指 offer 里的一道经典题,大家可以去力扣网站提交代码试一下。</p>
<pre><code>import java.util.HashMap;
public class LRUCache {
private int cacheSize;
private int currentSize;
private CacheNode head;
private CacheNode tail;
private HashMap&lt;Integer,CacheNode&gt; nodes;
class CacheNode{
CacheNode prev;
CacheNode next;
int key;
int value;
}
public LRUCache(int cacheSize){
cacheSize=cacheSize;
currentSize=0;
nodes=new HashMap&lt;&gt;(cacheSize);
}
public void set(Integer key,Integer value){
if(nodes.get(key)==null){ //添加新元素
CacheNode node=new CacheNode();
node.key=key;
node.value=value;
nodes.put(key,node);
//移动到表头
moveToHead(node);
//进行lru操作
if(currentSize&gt;cacheSize)
removeTail();
else
currentSize++;
}else{//更新元素值
CacheNode node=nodes.get(key);
//移动到表头
moveToHead(node);
node.value=value;
}
}
private void removeTail() {
if(tail!=null){
nodes.remove(tail.key);
if(tail.prev!=null) tail.prev.next=null;
tail=tail.prev;
}
}
private void moveToHead(CacheNode node){
//链表中间的元素
if(node.prev!=null){
node.prev.next=node.next;
}
if(node.next!=null){
node.next.prev=node.prev;
}
//移动到表头
node.prev=null;
if(head==null){
head=node;
}else{
node.next=head;
head.prev=node;
}
head=node;
//更新tail
//node就是尾部元素
if(tail==node){
//下移一位
tail=tail.prev;
}
//缓存里就一个元素
if(tail==null){
tail=node;
}
}
public int get(int key){
if(nodes.get(key)!=null){
CacheNode node=nodes.get(key);
moveToHead(node);
return node.value;
}
return 0;
}
}
</code></pre>
<h3>总结</h3>
<p>这一课时的内容主要介绍了缓存的几种失效策略并且分享了一个面试中的高频问题LRU 缓存实现。</p>
<p>缓存过期的策略来自操作系统,在我们的专栏中,对很多知识的展开都来自计算机原理、网络原理等底层技术,也从一个侧面反映了计算机基础知识的重要性。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/分布式技术原理与实战45讲-完/40 经典问题:先更新数据库,还是先更新缓存?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/分布式技术原理与实战45讲-完/42 负载均衡:一致性哈希解决了哪些问题?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709976f3ab873cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>