作者:禅与计算机程序设计艺术
1.简介
Redis是一个开源的内存数据库,其主要功能是提供高速读写速度。随着移动设备的普及,很多应用都需要在本地缓存数据,而使用Redis作为本地缓存可以降低对后端服务的依赖,提升应用的性能。本文将向您介绍Redis在移动端开发中的作用,并基于实际案例说明如何在应用中实现本地缓存功能。
2.概念术语说明
2.1 Redis
Redis是一个开源的内存数据库。它支持多种数据结构(strings、hashes、lists、sets、sorted sets),能够处理超大的数据量,且支持数据的持久化存储。
2.2 In-Memory Databases vs Relational Database Management Systems (RDBMS)
在讨论Redis之前,首先要理解一下RDBMS和In-Memory databases之间的区别。
RDBMS和关系型数据库管理系统(Relational DataBase Management System)一样,也属于关系型数据库。不同的是,RDBMS的实现通常是服务器端,而In-Memory databases的实现则是在客户端进行。也就是说,当应用程序需要访问或修改数据时,In-Memory databases就直接从内存中读取或写入数据;而RDBMS则需要通过网络发送请求到服务器端执行查询或修改操作。
In-Memory databases的优点是快速读取和写入,但同时也有一些缺点:它们无法处理海量数据,而且对于复杂查询和事务处理等操作来说效率不够高。相反,RDBMS提供了更好的容错性、安全性和一致性保障,这些特性对于大型网站和应用来说非常重要。
2.3 Mobile App Development
移动应用开发(Mobile Application Development,MAD)是一个跨平台的软件工程领域,包括iOS、Android等多个版本,开发者们都在不断完善和更新。由于移动设备资源有限、运行速度慢、硬件配置不足等原因,因此,很多开发者都会选择用数据库缓存的方式来优化应用的性能。本文基于这个理念,阐述了Redis在移动端开发中的作用。
3.核心算法原理和具体操作步骤以及数学公式讲解
3.1 使用Redis缓存用户信息
我们可以把用户的信息缓存在Redis中,这样就可以避免频繁的与后端服务通信,加快应用的响应速度。
-
安装Redis
Redis可以使用不同的安装方式,如使用Docker镜像、源码包安装等。笔者推荐使用源码包安装,如果您的操作系统不确定,可以参考官方文档。 -
配置Redis
安装完成后,启动Redis服务。一般情况下,Redis会默认监听6379端口,可以根据自己的需求更改端口号。配置Redis可以通过redis.conf配置文件或命令行参数设置。比如,我们可以设置Redis最大允许连接数、超时时间、密码、数据库个数等。 -
创建键值对
接下来,我们可以使用Redis的API或者客户端工具创建键值对。键名可以使用用户ID,值可以是一个JSON对象,包含用户的所有相关信息。例如:
{
"id": "123",
"name": "John Doe",
"email": "johndoe@example.com"
}
-
设置过期时间
如果我们希望Redis缓存某个键值的有效期较短,可以设置一个过期时间。过期时间可以在创建键值对的时候设置,也可以使用其他命令动态设置。 -
缓存重建机制
如果某些情况导致Redis中的数据损坏或丢失,为了防止出现意外,我们需要设计一个缓存重建机制。比如,我们可以在应用启动时检查是否存在丢失的缓存数据,并且重新加载它们。
3.2 实现搜索功能
搜索功能是移动应用的一个基础功能。用户可以在应用内输入关键字查找相关内容,比如商品名称、商家名称等。我们可以通过Redis实现搜索功能。
- 索引库
首先,我们需要创建一个索引库,其中包含所有可搜索的内容。每一条记录都是一个JSON对象,包含搜索关键词和对应的文档ID。例如:
{
"keyword": "iphone x",
"docId": "123456789"
},
{
"keyword": "galaxy s9",
"docId": "987654321"
},
...
- 搜索引擎
然后,我们需要建立一个搜索引擎,它可以接收用户的搜索请求,并返回相应的结果。搜索引擎可以分成两部分:前端搜索模块和后台搜索模块。
- 前端搜索模块:负责收集用户的搜索请求、展示搜索结果页面、处理用户交互事件。
- 后台搜索模块:负责处理搜索请求、检索索引库中的文档、排序和过滤结果。后台搜索模块可以利用多线程技术并行处理搜索请求,提升搜索响应速度。
- 缓存搜索结果
为了提升应用的性能,我们可以缓存搜索结果。当用户第一次发起搜索请求时,后台搜索模块会检索索引库,并生成相应的搜索结果。这些结果可以被缓存到Redis中,并设置一个合适的过期时间。下次相同的搜索请求只需从Redis中获取结果即可。
3.3 数据分页
在移动端应用中,经常会遇到需要显示大量数据的场景,如首页展示全站文章列表。对于这种场景,我们可以采用分页的策略,每次只取一页的数据,并使用滑动加载的方式展示更多数据。
- 生成分页结果
分页逻辑比较简单,我们只需要按照指定的大小和当前页码计算出对应范围的文档ID列表即可。例如,假设每页10条数据,当前页码为3。那么,我们可以把文档ID按顺序分组为如下几个部分:
[101-110], [121-130]... [491-500]
-
从缓存中获取数据
接下来,我们可以从Redis中获取相应的数据,并通过模板渲染器生成HTML页面。 -
缓存分页结果
最后,我们可以把分页后的结果缓存到Redis中,并设置一个合适的过期时间。下次相同的请求只需要从Redis中获取结果,不需要再进行分页运算。
3.4 使用Redis实现排行榜功能
排行榜功能可以帮助用户快速找到最受欢迎或热门的商品。我们可以结合Redis实现该功能。
- 把排行榜数据存入Redis
首先,我们需要把排行榜数据存入Redis。每个元素是一个JSON对象,包含排行榜项的关键词、热度值等。例如:
{
"keyword": "apple iphone",
"hotValue": 1000
},
{
"keyword": "huawei p smart",
"hotValue": 900
},
...
-
创建排序视图
然后,我们可以创建排序视图,它可以展示排行榜的前N个元素,并根据用户的点击行为对元素进行排序。排序视图可以调用后台搜索模块的接口,并通过Redis缓存的数据进行排序。 -
定时刷新排行榜
最后,我们还可以设置一个定时任务,定期刷新排行榜,确保数据始终处于最新状态。定时任务可以调用后台搜索模块的接口,并把新数据存入Redis中。
4.具体代码实例和解释说明
在实际业务实施过程中,我们可能会遇到很多实际的问题,本节将介绍具体的代码实例和解释说明。
4.1 在Android应用中使用Redis缓存用户信息
本例中,我们使用Redis缓存用户信息。我们创建一个用户类User,用于表示用户相关的信息。User类包含用户名、邮箱、头像URL等属性。
public class User {
private String id; // 用户ID
private String name; // 用户姓名
private String email; // 用户邮箱
public User(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
4.1.1 配置Redis
如果没有特殊要求,可以直接使用Redis的默认配置。下面是Redis的配置文件redis.conf的示例:
bind 127.0.0.1 # 只允许本机连接
port 6379 # 默认端口
timeout 0 # 不设置超时时间
tcp-keepalive 300 # TCP保活间隔
loglevel notice # 日志级别
logfile /var/log/redis/redis.log # 日志文件位置
databases 1 # 默认数据库个数
always-show-logo yes # 是否显示图标
如果需要调整配置,可以在redis.conf文件中修改,保存后执行redis-server reload命令使配置生效。
4.1.2 创建Redis连接池
为了减少重复的代码编写,我们可以封装一个Redis连接池。下面是一个简单的Redis连接池的实现:
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisUtils {
private static final int MAX_ACTIVE = 1024; // 最大连接数
private static final int MAX_IDLE = 20; // 最大空闲连接数
private static final long MAX_WAIT = -1L; // 最大等待时间
private static final boolean TEST_ON_BORROW = false; // 碰到异常时是否进行测试
private static final String HOST = "localhost"; // Redis主机地址
private static final int PORT = 6379; // Redis端口号
private static final String PASSWORD = null; // Redis密码,如果没有填null
private static JedisPool jedisPool;
static {
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
if (MAX_WAIT > 0) {
config.setMaxWaitMillis(MAX_WAIT);
}
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, HOST, PORT,
Protocol.DEFAULT_TIMEOUT, PASSWORD);
} catch (Exception e) {
throw new ExceptionInInitializerError("Cannot create Jedis pool");
}
}
/**
* 获取一个Jedis连接
*/
public synchronized static Jedis getJedis() throws Exception {
if (jedisPool == null) {
throw new Exception("Redis connection pool not initialized!");
}
return jedisPool.getResource();
}
/**
* 释放一个Jedis连接
*/
public static void returnResource(final Jedis resource) {
if (resource!= null) {
resource.close();
}
}
}
4.1.3 测试Redis缓存用户信息
下面我们测试Redis缓存用户信息的效果。
try {
JedisUtils.getJedis().set("user:123", "{\"name\":\"John Doe\",\"email\":\"johndoe@example.com\"}");
} finally {
JedisUtils.returnResource(null);
}
上面的代码将用户ID为123的用户信息缓存到Redis中。
String userJsonStr = JedisUtils.getJedis().get("user:123");
User user = JSONObject.parseObject(userJsonStr, User.class);
System.out.println("Name:" + user.getName());
System.out.println("Email:" + user.getEmail());
上面代码从Redis中获取用户ID为123的用户信息,并解析为User对象,打印用户的姓名和邮箱。
4.1.4 添加过期时间
如果我们希望缓存的用户信息在一段时间后自动过期,可以使用Redis的expire命令设置过期时间。
long ttlSeconds = JedisUtils.getJedis().ttl("user:123");
if (ttlSeconds <= 0) {
JedisUtils.getJedis().expire("user:123", 3600*24); // 缓存有效期设置为一天
}
上面代码先获取用户123的剩余有效期,如果小于等于0,则设置其过期时间为一天。
4.1.5 缓存重建机制
缓存重建机制指的是在出现意外丢失缓存数据时,尝试从源头重新构建缓存数据。本例中,我们可以定期从后台服务拉取用户信息,并更新到Redis中。
List<User> usersFromRemoteServer = getUserListFromRemoteServer();
for (User user : usersFromRemoteServer) {
try {
JedisUtils.getJedis().set("user:" + user.getId(), JSONObject.toJSONString(user));
JedisUtils.getJedis().expire("user:" + user.getId(), 3600*24); // 设置缓存过期时间为一天
} catch (Exception e) {
logger.error("Failed to cache user info for user ID: " + user.getId());
}
}
上面代码使用远程服务拉取用户信息,循环遍历每个用户,并把用户信息缓存到Redis中。如果失败,则输出错误日志。
4.2 Android应用实现搜索功能
本例中,我们实现搜索功能,用户可以通过输入关键词搜索商品名称、商家名称等。
4.2.1 索引库
为了实现搜索功能,我们需要建立一个索引库。索引库中包含所有的搜索关键词和对应的文档ID。下面是一个简单的索引库的实现:
public class SearchIndex {
private Map<String, List<Integer>> indexMap = new HashMap<>();
public SearchIndex(List<Document> documents) {
initIndex(documents);
}
public List<Integer> search(String keyword) {
List<Integer> docIds = indexMap.get(keyword);
if (docIds == null) {
return Collections.emptyList();
} else {
return docIds;
}
}
private void initIndex(List<Document> documents) {
for (int i = 0; i < documents.size(); i++) {
Document document = documents.get(i);
for (String keyword : document.getKeywords()) {
addKeywordToDocIdMapping(document.getId(), keyword);
}
}
}
private void addKeywordToDocIdMapping(int docId, String keyword) {
List<Integer> docIds = indexMap.computeIfAbsent(keyword, k -> new ArrayList<>());
docIds.add(docId);
}
}
SearchIndex类构造函数传入了一个文档列表,初始化索引库。索引库是一个HashMap,键为搜索关键词,值为文档ID的列表。initIndex方法用来构建索引库,循环遍历文档列表,添加每个文档的关键词到索引库。search方法通过关键词搜索文档ID。
4.2.2 搜索引擎
搜索引擎包括前端搜索模块和后台搜索模块。前端搜索模块负责收集用户的搜索请求,处理用户交互事件,并展示搜索结果页面。后台搜索模块负责处理搜索请求,检索索引库,排序和过滤结果,并缓存结果到Redis中。
4.2.2.1 前端搜索模块
前端搜索模块是一个Activity,包含搜索框、搜索按钮、结果列表三个组件。搜索框用于收集用户的搜索请求,结果列表用于展示搜索结果。下面是搜索框的示例代码:
<EditText
android:id="@+id/et_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Search..." />
findViewById(R.id.et_search).setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchHandler();
return true;
}
return false;
});
private void searchHandler() {
String keyword = findViewById(R.id.et_search).getText().toString().trim();
if (!TextUtils.isEmpty(keyword)) {
startSearching(keyword);
}
}
private void startSearching(String keyword) {
Intent intent = new Intent(this, SearchResultActivity.class);
Bundle bundle = new Bundle();
bundle.putString("keyword", keyword);
intent.putExtras(bundle);
startActivityForResult(intent, REQUEST_CODE_SEARCHING);
}
搜索按钮的点击事件处理函数searchHandler()调用startSearching(),通过传递搜索关键词到SearchResultActivity。
4.2.2.2 后台搜索模块
后台搜索模块是一个Service,它负责处理搜索请求、检索索引库,排序和过滤结果,并缓存结果到Redis中。下面是后台搜索模块的实现:
public class SearchEngineService extends Service {
private static final String TAG = "SearchEngineService";
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind()");
return mBinder;
}
private final ISearchEngine.Stub mBinder = new ISearchEngine.Stub() {
@Override
public List<Integer> searchDocuments(String keyword) throws RemoteException {
return SearchEngineManager.getInstance().search(keyword);
}
};
}
后台搜索模块提供一个searchDocuments()接口,用于接收前端搜索模块的搜索请求,检索索引库,排序和过滤结果,并缓存结果到Redis中。
下面是后台搜索模块的具体实现:
public class SearchEngineManager implements LifecycleObserver {
private static final String TAG = "SearchEngineManager";
private static volatile SearchEngineManager instance;
private Executor executor;
private Handler handler;
private HandlerThread workerThread;
private MessageQueue messageQueue;
private Map<String, FutureTask<List<Integer>>> taskMap;
private Context context;
public static SearchEngineManager getInstance() {
if (instance == null) {
synchronized (SearchEngineManager.class) {
if (instance == null) {
instance = new SearchEngineManager();
}
}
}
return instance;
}
private SearchEngineManager() {
lifecycle.addObserver(this);
}
private void initialize() {
if (executor == null || handler == null || workerThread == null) {
executor = Executors.newCachedThreadPool();
workerThread = new HandlerThread("Worker Thread");
workerThread.start();
messageQueue = Looper.myQueue();
handler = new Handler(workerThread.getLooper());
taskMap = new ConcurrentHashMap<>();
}
}
public List<Integer> search(String keyword) {
List<Integer> result = Collections.emptyList();
if (TextUtils.isEmpty(keyword)) {
return result;
}
FutureTask<List<Integer>> futureTask = taskMap.get(keyword);
if (futureTask == null) {
futureTask = new FutureTask<>(() -> loadResults(keyword), null);
taskMap.put(keyword, futureTask);
executor.execute(futureTask);
} else {
try {
result = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return result;
}
private List<Integer> loadResults(String keyword) {
List<Integer> results = Collections.emptyList();
try {
List<Document> documents = queryDataFromBackend(keyword);
Sorter sorter = new Sorter();
Collection<Rankable> rankables = convertToRankables(documents);
rankables = sorter.sort(rankables);
results = convertToList(rankables);
cacheResults(results, keyword);
} catch (IOException e) {
e.printStackTrace();
}
return results;
}
private List<Rankable> convertToRankables(List<Document> documents) {
List<Rankable> rankables = new ArrayList<>(documents.size());
for (Document document : documents) {
Rankable rankable = new Rankable(document.getId(), calculateScore(document));
rankables.add(rankable);
}
return rankables;
}
private double calculateScore(Document document) {
/* 根据文档内容计算得分 */
return 1.0;
}
private List<Document> queryDataFromBackend(String keyword) throws IOException {
/* 通过后端服务查询数据 */
return Arrays.asList(
new Document(1, "iPhone X"),
new Document(2, "Huawei P Smart")
);
}
private List<Integer> convertToList(Collection<? extends Rankable> collection) {
List<Integer> list = new ArrayList<>(collection.size());
for (Rankable r : collection) {
list.add(r.getId());
}
return list;
}
private void cacheResults(List<Integer> results, String keyword) {
try {
JedisUtils.getJedis().setex("searchresult_" + keyword, SEARCH_RESULT_EXPIRE_TIME, JSON.toJSONBytes(results));
} catch (Exception e) {
e.printStackTrace();
}
}
private void readCache(String keyword) {
String jsonStr = null;
try {
byte[] bytes = JedisUtils.getJedis().get(("searchresult_" + keyword));
if (bytes!= null) {
jsonStr = new String(bytes);
}
} catch (Exception e) {
e.printStackTrace();
}
if (!StringUtils.isEmpty(jsonStr)) {
Type type = new TypeToken<List<Integer>>(){}.getType();
List<Integer> cachedResults = JSON.parseArray(jsonStr, Integer.class);
deliverSearchResults(cachedResults, keyword);
} else {
requestSearchResults(keyword);
}
}
private void requestSearchResults(String keyword) {
Intent intent = new Intent(context, SearchEngineService.class);
intent.putExtra("keyword", keyword);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + CACHE_MISS_THRESHOLD, pendingIntent);
}
private void deliverSearchResults(List<Integer> results, String keyword) {
sendMessage(message -> message.obj = Pair.create(results, keyword));
}
protected void handleMessage(Message message) {
Object obj = message.obj;
if (obj instanceof Pair) {
Pair pair = (Pair) obj;
List<Integer> results = (List<Integer>) pair.first;
String keyword = (String) pair.second;
deliverSearchResults(results, keyword);
taskMap.remove(keyword);
}
}
private void sendMessage(Consumer<Message> consumer) {
handler.post(() -> consumer.accept(handler.obtainMessage()));
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
protected void onAppForegrounded() {
Log.d(TAG, "onAppForegrounded()");
initialize();
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
protected void onAppBackgrounded() {
Log.d(TAG, "onAppBackgrounded()");
stopBackgroundTasks();
}
private void stopBackgroundTasks() {
cancelPendingTasks();
}
private void cancelPendingTasks() {
for (FutureTask futureTask : taskMap.values()) {
futureTask.cancel(true);
}
taskMap.clear();
}
}
SearchEngineManager类是一个单例类,它的实例负责管理后台搜索模块的生命周期,处理消息,请求后台搜索服务,缓存搜索结果,以及处理结果回调。
后台搜索模块的启动流程是:
- 当后台搜索模块被激活时,后台搜索模块创建并启动后台工作线程。
- 当应用进入前台时,后台搜索模块调用SearchEngineManager的onAppForegrounded()方法,初始化后台搜索模块内部的状态变量和后台工作线程。
- 当用户输入搜索关键词时,前端搜索模块调用后台搜索模块的search()方法,后台搜索模块判断是否有搜索关键词对应的缓存结果,如果没有,则触发后台搜索,否则,直接从缓存中读取结果。
- 当后台搜索模块收到用户搜索请求时,后台搜索模块通过FutureTask异步加载数据。
- 当后台搜索完成时,后台搜索模块通过sendMessage()发送消息给主线程,通知主线程显示搜索结果。
- 如果缓存搜索结果为空,后台搜索模块创建PendingIntent,并注册系统闹钟,在指定的时间之后发送PendingIntent。
- 当用户点击搜索按钮时,前端搜索模块触发后台搜索模块的search()方法,后台搜索模块从后台工作队列取出对应的FutureTask,并判断FutureTask是否已经完成。如果完成,则从FutureTask中获取搜索结果,并调用deliverSearchResults()方法,并把搜索结果缓存到Redis中。
- 当用户输入新的搜索关键词时,后台搜索模块取消已有的后台搜索任务,并重新启动新的后台搜索任务。
- 当后台搜索模块收到系统闹钟广播时,后台搜索模块判断是否有正在进行的后台搜索任务。如果有,则忽略此次广播;如果无,则创建新的后台搜索任务。
- 当用户停止应用时,后台搜索模块通过lifecycle观察者模式处理生命周期事件,调用stopBackgroundTasks()方法,停止后台搜索任务。
后台搜索模块的关键算法有:
- 请求搜索结果:后台搜索模块使用FutureTask异步加载数据。
- 排序和过滤结果:后台搜索模块使用归纳推理法排序结果,并过滤掉排名过低的结果。
- 缓存搜索结果:后台搜索模块把搜索结果缓存到Redis中。
- 重启后台搜索任务:后台搜索模块取消已有的后台搜索任务,并重新启动新的后台搜索任务。
4.3 Android应用实现分页功能
本例中,我们实现分页功能,用户可以查看指定页码的商品列表。
4.3.1 生成分页结果
分页逻辑比较简单,我们只需要按照指定的大小和当前页码计算出对应范围的文档ID列表即可。下面是一个分页生成器的实现:
public class PaginationGenerator {
private int pageSize;
private int totalCount;
private int currentPageNumber;
public PaginationGenerator(int pageSize, int totalCount, int currentPageNumber) {
this.pageSize = pageSize;
this.totalCount = totalCount;
this.currentPageNumber = currentPageNumber;
}
public Page generatePage() {
int startIndex = (currentPageNumber - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, totalCount);
List<Integer> pageDocIds = new ArrayList<>();
for (int i = startIndex; i < endIndex; i++) {
pageDocIds.add(i + 1);
}
return new Page(pageDocIds, currentPageNumber, getTotalPageCount());
}
private int getTotalPageCount() {
return (int) Math.ceil((double) totalCount / pageSize);
}
}
PaginationGenerator类通过传入分页参数,计算出当前页的文档ID列表。generatePage()方法调用getTotalPageCount()方法计算总页数,并返回Page对象。Page类是一个容器类,包含当前页的文档ID列表、当前页码、总页码。
4.3.2 从缓存中获取数据
接下来,我们可以从Redis中获取相应的数据,并通过模板渲染器生成HTML页面。这里,我们假设商品详情数据存放在键值为product:{docId}的Redis哈希表中,其中{docId}表示商品ID。
String productHashKey = "product:" + docId;
byte[] productByteArray = JedisUtils.getJedis().hget(productHashKey.getBytes(), PRODUCT_INFO_FIELD.getBytes());
String productJsonStr = new String(productByteArray);
ProductDetail productDetail = JSONObject.parseObject(productJsonStr, ProductDetail.class);
上面的代码先从Redis中获取商品ID为docId的产品详情数据,并解析为ProductDetail对象。
4.3.3 缓存分页结果
最后,我们可以把分页后的结果缓存到Redis中,并设置一个合适的过期时间。下次相同的请求只需要从Redis中获取结果,不需要再进行分页运算。
long expireTime = TimeUnit.DAYS.toSeconds(30); // 设置缓存过期时间为30天
String paginationCacheKey = "paginatedproducts:" + currentPageNumber;
byte[] paginatedProductsByteArray = JSON.toJSONBytes(paginationGenerator.generatePage());
JedisUtils.getJedis().setex(paginationCacheKey, expireTime, paginatedProductsByteArray);
上面的代码先生成分页结果,并将结果序列化为字节数组。然后,将分页结果缓存到Redis中,并设置过期时间为30天。
4.3.4 渲染分页HTML
在浏览器中打开分页HTML页面,可以看到商品列表。下面是一个分页HTML页面的示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Paginated Products</title>
</head>
<body>
{% for docId in products %}
<div>{{ render_product(docId) }}</div>
{% endfor %}
</body>
</html>
{% raw %}{{ render_product(docId) }}
{% endraw %}标记是一个模板标签,用于渲染商品详情页。{% raw %}{% for docId in products %}{% endraw %}块用于循环遍历分页结果,{% raw %}render_product()
{% endraw %}是一个自定义的模板函数,用于渲染商品详情页。
模板渲染器的具体实现如下:
Template template = configuration.getTemplate("template.html");
Writer writer = new OutputStreamWriter(response.getOutputStream());
template.process(rootMap, writer);
writer.flush();
模板渲染器使用Freemarker作为模板引擎。下面是渲染器的配置:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
cfg.setDefaultEncoding("UTF-8");
cfg.setClassForTemplateLoading(getClass(), "/templates");
cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_28).build());
er_product()`{% endraw %}是一个自定义的模板函数,用于渲染商品详情页。
模板渲染器的具体实现如下:
```java
Template template = configuration.getTemplate("template.html");
Writer writer = new OutputStreamWriter(response.getOutputStream());
template.process(rootMap, writer);
writer.flush();
模板渲染器使用Freemarker作为模板引擎。下面是渲染器的配置:
Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
cfg.setDefaultEncoding("UTF-8");
cfg.setClassForTemplateLoading(getClass(), "/templates");
cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_28).build());