如何使用Redis 在移动端应用开发中实现本地缓存功能 Redis and Mobile Development:How to Use Redis

作者:禅与计算机程序设计艺术

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中,这样就可以避免频繁的与后端服务通信,加快应用的响应速度。

  1. 安装Redis
    Redis可以使用不同的安装方式,如使用Docker镜像、源码包安装等。笔者推荐使用源码包安装,如果您的操作系统不确定,可以参考官方文档。

  2. 配置Redis
    安装完成后,启动Redis服务。一般情况下,Redis会默认监听6379端口,可以根据自己的需求更改端口号。配置Redis可以通过redis.conf配置文件或命令行参数设置。比如,我们可以设置Redis最大允许连接数、超时时间、密码、数据库个数等。

  3. 创建键值对
    接下来,我们可以使用Redis的API或者客户端工具创建键值对。键名可以使用用户ID,值可以是一个JSON对象,包含用户的所有相关信息。例如:

{
  "id": "123",
  "name": "John Doe",
  "email": "johndoe@example.com"
}
  1. 设置过期时间
    如果我们希望Redis缓存某个键值的有效期较短,可以设置一个过期时间。过期时间可以在创建键值对的时候设置,也可以使用其他命令动态设置。

  2. 缓存重建机制
    如果某些情况导致Redis中的数据损坏或丢失,为了防止出现意外,我们需要设计一个缓存重建机制。比如,我们可以在应用启动时检查是否存在丢失的缓存数据,并且重新加载它们。

3.2 实现搜索功能

搜索功能是移动应用的一个基础功能。用户可以在应用内输入关键字查找相关内容,比如商品名称、商家名称等。我们可以通过Redis实现搜索功能。

  1. 索引库
    首先,我们需要创建一个索引库,其中包含所有可搜索的内容。每一条记录都是一个JSON对象,包含搜索关键词和对应的文档ID。例如:
{
  "keyword": "iphone x",
  "docId": "123456789"
},
{
  "keyword": "galaxy s9",
  "docId": "987654321"
},
...
  1. 搜索引擎
    然后,我们需要建立一个搜索引擎,它可以接收用户的搜索请求,并返回相应的结果。搜索引擎可以分成两部分:前端搜索模块和后台搜索模块。
  • 前端搜索模块:负责收集用户的搜索请求、展示搜索结果页面、处理用户交互事件。
  • 后台搜索模块:负责处理搜索请求、检索索引库中的文档、排序和过滤结果。后台搜索模块可以利用多线程技术并行处理搜索请求,提升搜索响应速度。
  1. 缓存搜索结果
    为了提升应用的性能,我们可以缓存搜索结果。当用户第一次发起搜索请求时,后台搜索模块会检索索引库,并生成相应的搜索结果。这些结果可以被缓存到Redis中,并设置一个合适的过期时间。下次相同的搜索请求只需从Redis中获取结果即可。

3.3 数据分页

在移动端应用中,经常会遇到需要显示大量数据的场景,如首页展示全站文章列表。对于这种场景,我们可以采用分页的策略,每次只取一页的数据,并使用滑动加载的方式展示更多数据。

  1. 生成分页结果
    分页逻辑比较简单,我们只需要按照指定的大小和当前页码计算出对应范围的文档ID列表即可。例如,假设每页10条数据,当前页码为3。那么,我们可以把文档ID按顺序分组为如下几个部分:
[101-110], [121-130]... [491-500]
  1. 从缓存中获取数据
    接下来,我们可以从Redis中获取相应的数据,并通过模板渲染器生成HTML页面。

  2. 缓存分页结果
    最后,我们可以把分页后的结果缓存到Redis中,并设置一个合适的过期时间。下次相同的请求只需要从Redis中获取结果,不需要再进行分页运算。

3.4 使用Redis实现排行榜功能

排行榜功能可以帮助用户快速找到最受欢迎或热门的商品。我们可以结合Redis实现该功能。

  1. 把排行榜数据存入Redis
    首先,我们需要把排行榜数据存入Redis。每个元素是一个JSON对象,包含排行榜项的关键词、热度值等。例如:
{
  "keyword": "apple iphone",
  "hotValue": 1000
},
{
  "keyword": "huawei p smart",
  "hotValue": 900
},
...
  1. 创建排序视图
    然后,我们可以创建排序视图,它可以展示排行榜的前N个元素,并根据用户的点击行为对元素进行排序。排序视图可以调用后台搜索模块的接口,并通过Redis缓存的数据进行排序。

  2. 定时刷新排行榜
    最后,我们还可以设置一个定时任务,定期刷新排行榜,确保数据始终处于最新状态。定时任务可以调用后台搜索模块的接口,并把新数据存入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类是一个单例类,它的实例负责管理后台搜索模块的生命周期,处理消息,请求后台搜索服务,缓存搜索结果,以及处理结果回调。

后台搜索模块的启动流程是:

  1. 当后台搜索模块被激活时,后台搜索模块创建并启动后台工作线程。
  2. 当应用进入前台时,后台搜索模块调用SearchEngineManager的onAppForegrounded()方法,初始化后台搜索模块内部的状态变量和后台工作线程。
  3. 当用户输入搜索关键词时,前端搜索模块调用后台搜索模块的search()方法,后台搜索模块判断是否有搜索关键词对应的缓存结果,如果没有,则触发后台搜索,否则,直接从缓存中读取结果。
  4. 当后台搜索模块收到用户搜索请求时,后台搜索模块通过FutureTask异步加载数据。
  5. 当后台搜索完成时,后台搜索模块通过sendMessage()发送消息给主线程,通知主线程显示搜索结果。
  6. 如果缓存搜索结果为空,后台搜索模块创建PendingIntent,并注册系统闹钟,在指定的时间之后发送PendingIntent。
  7. 当用户点击搜索按钮时,前端搜索模块触发后台搜索模块的search()方法,后台搜索模块从后台工作队列取出对应的FutureTask,并判断FutureTask是否已经完成。如果完成,则从FutureTask中获取搜索结果,并调用deliverSearchResults()方法,并把搜索结果缓存到Redis中。
  8. 当用户输入新的搜索关键词时,后台搜索模块取消已有的后台搜索任务,并重新启动新的后台搜索任务。
  9. 当后台搜索模块收到系统闹钟广播时,后台搜索模块判断是否有正在进行的后台搜索任务。如果有,则忽略此次广播;如果无,则创建新的后台搜索任务。
  10. 当用户停止应用时,后台搜索模块通过lifecycle观察者模式处理生命周期事件,调用stopBackgroundTasks()方法,停止后台搜索任务。

后台搜索模块的关键算法有:

  1. 请求搜索结果:后台搜索模块使用FutureTask异步加载数据。
  2. 排序和过滤结果:后台搜索模块使用归纳推理法排序结果,并过滤掉排名过低的结果。
  3. 缓存搜索结果:后台搜索模块把搜索结果缓存到Redis中。
  4. 重启后台搜索任务:后台搜索模块取消已有的后台搜索任务,并重新启动新的后台搜索任务。

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());
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

禅与计算机程序设计艺术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值