更新要点
在容器编排中引入Redis 7.2.4
将Redis服务作为MySQL缓冲层
修复客户端部分bug,并使用Release模式重新编译
为什么引入Redis缓存 根据时间局部性原理,访问过的数据,短时间内大概率会被再次访问,由于服务端的业务处理运行在内存中,而数据库的访问数据会发生硬盘IO,存在巨大的速度差异,导致性能瓶颈,所以我们在数据库与业务层中引入数据缓存,提高数据的访问速率。
创建Redis容器 为了更方便地使用Redis服务,我们使用Redis的Docker镜像生成一个容器来提供服务。比如我们使用镜像redis:7.2.4
下面是docker-compose.yml的部分代码,特别的 ,我们的业务层容器依赖于redis容器先启动,所以也要修改server
容器的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 services: server: build: context: ./server image: btygoose environment: - TZ=Asia/Shanghai - LANG=C.UTF-8 depends_on: db: condition: service_healthy redismaster: condition: service_healthy networks: - btygoose_net server2: image: btygoose environment: - TZ=Asia/Shanghai - LANG=C.UTF-8 depends_on: db: condition: service_healthy redismaster: condition: service_healthy networks: - btygoose_net redismaster: image: redis:7.2.4 restart: always volumes: - ./redis/data:/data - ./redis/conf/redis.conf:/usr/local/etc/redis/redis.conf command: redis-server /usr/local/etc/redis/redis.conf environment: - TZ=Asia/Shanghai - REDIS_PASSWORD=redispass123 - maxmemory=512mb - maxmemory-policy=allkeys-lru healthcheck: test: ["CMD" , "redis-cli" , "-a" , "$$REDIS_PASSWORD" , "ping" ] interval: 10s timeout: 5s retries: 3 networks: - btygoose_net
编写Dockerfile搭建环境 因为这里选用了编译安装的方式安装redis-plus-plus
库,所以Dockerfile
写起来会较为复杂,我们至少有如下步骤:
apt安装hiredis
git clone项目到本地
cmake构建
make编译
make install安装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 FROM ubuntu:22.04 AS build1RUN apt update -y && apt install -y g++ \ make \ git \ cmake \ libmysqlcppconn-dev \ libjsoncpp-dev \ libboost-all-dev\ libgflags-dev \ libfmt-dev \ libspdlog-dev \ libhiredis-dev WORKDIR /redispp RUN git clone https://github.com/sewenew/redis-plus-plus.git RUN mkdir -p /redispp/redis-plus-plus/build WORKDIR /redispp/redis-plus-plus/build RUN cmake .. RUN make RUN make install RUN ldconfig COPY ./src* /src WORKDIR /src RUN make && mkdir logs CMD ["/src/server" ,"--flagfile=server.conf" ]
编写Redis客户端 我们也可以在开发环境中配置安装redis-plus-plus方便写代码
我们封装一个btyGoose::RedisClient
类,专门负责向业务层提供服务和向Redis层请求服务
我们在这个版本中要实现的接口有:
RedisClient.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #pragma once #include <sw/redis++/redis.h> #include <unordered_set> #include <vector> #include <unordered_map> #include <memory> #include "CoreData.hpp" #include "logger.hpp" namespace btyGoose{using std::string;using std::shared_ptr;using std::make_shared;using std::vector;class RedisClient { const std::string account_prefix = "data:Account:" ; const std::string dish_prefix = "data:Dish:" ; const std::string order_prefix = "data:Order:" ; const std::string history_prefix = "data:History:" ; const std::chrono::seconds expire_time = std::chrono::seconds (300 ); public : RedisClient (const string&ip,const uint16_t port,const uint16_t db,const bool keep_alive,const string&password) { LOG_INFO ("即将连接Redis服务器,地址 {}:{}" ,ip,port); sw::redis::ConnectionOptions opts; opts.host = ip; opts.port = port; opts.db = db; opts.keep_alive = keep_alive; opts.password = password; _redis = make_shared <sw::redis::Redis>(sw::redis::Redis (opts)); } public : bool isOK () { try { auto pong = _redis->ping (); if (pong == "PONG" ) { return true ; } } catch (const sw::redis::Error &e) { LOG_WARN ("Redis连接失败:{}" ,e.what ()); return false ; } return false ; } void flushall () { _redis->flushall (); } void setOrder (const data::Order& order) ; void setOrderDishListJson (const string&order_id,const string& dish_list_json) ; string getOrderDishListJson (const string&order_id) ; data::Order getOrderById (const string& id) ; void setOrderList (const vector<data::Order> order_list) ; void setOrderListByIdDone (const string&id) ; vector<data::Order> getOrderListByMerchant (const string&id) ; vector<data::Order> getOrderListByMerchantWaiting (const string&id) ; vector<data::Order> getOrderListByConsumer (const string&id) ; bool hasOrderListByUserId (const string& id) ; void delOrderById (const string&id) ; data::Dish getDishById (const string&id) ; void delDishById (const string&id) ; string getDishListJsonByMerchant (const string&id) ; void setDishListJsonByMerchant (const string&id,const string&json) ; void delDishListJsonByMerchant (const string&id) ; void setDish (const data::Dish& dish) ; data::Account getAccountByName (const string&name) ; data::Account getAccountById (const string&id) ; data::Account getAccountByPhone (const string&phone) ; void setAccount (const data::Account& acc) ; private : shared_ptr<sw::redis::Redis> _redis; }; }
当然,对于缓存的具体实现,有如下问题要考虑
如何存储结构化的数据 像Order
,Dish
这种结构化的数据,在Redis中我们可以选择如下比较合理的存储方式
用哈希表存储
序列化后存储
其中用哈希存储的灵活性较高,易修改,而序列化后存储则是数据的完整强,不会出现误修改的问题,因此我们得出如下策略
对于Dish
,Account
这种简单,不易改变的数据,两种方法皆可,我们使用更为简单的哈希表存储
对于Order
这种容易动态改变的数据,我们使用哈希表存储
对于vector<Order>
这种与Order
相关联的数据,我们使用set
无序集合来存储它们的key
对于vector<OrderDish>
这种要求数据完整性强的,数据不会变动的类型,我们使用序列化后存储
以下是项目中的实际例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 void RedisClient::setAccount (const data::Account& acc) { unordered_map<string,string> m; m["uuid" ] = acc.uuid; m["name" ] = acc.name; m["password" ] = acc.password; m["nickname" ] = acc.nickname; m["icon" ] = acc.icon; m["type" ] = to_string (static_cast <int >(acc.type)); m["balance" ] = to_string (acc.balance); m["phone" ] = acc.phone; m["level" ] = to_string (static_cast <int >(acc.level)); _redis->hmset (account_prefix+"hash:" +acc.name,m.begin (),m.end ()); _redis->set (account_prefix+"id:" +acc.uuid,account_prefix+acc.name); _redis->set (account_prefix+"phone:" +acc.phone,account_prefix+acc.name); } void RedisClient::setOrder (const data::Order&order) { unordered_map<string,string> m; m["merchant_id" ] = order.merchant_id; m["merchant_name" ] = order.merchant_name; m["consumer_id" ] = order.consumer_id; m["consumer_name" ] = order.consumer_name; m["time" ] = order.time; m["level" ] = to_string (static_cast <int >(order.level)); m["pay" ] = to_string (order.pay); m["uuid" ] = order.uuid; m["status" ] = to_string (static_cast <int >(order.status)); m["sum" ] = to_string (order.sum); _redis->hset (order_prefix+"id:" +order.uuid,m.begin (),m.end ()); _redis->sadd (order_prefix+"merchant:" +order.merchant_id,order.uuid); _redis->sadd (order_prefix+"consumer:" +order.consumer_id,order.uuid); } void RedisClient::setOrderDishListJson (const string&order_id,const string& dish_list_json) { _redis->set (order_prefix+"dishlist:" +order_id,dish_list_json); }
缓存一致性问题 一旦引入缓存,缓存一致性问题就绕不开了。这里采用偏向于简单的解决方法
对于一般的数据,我们会把增删改即时同步到数据库中
对于OrderList
这样的数据,我们采用额外的集合来标记有效的key
,当发生增删改时,使key
失效,对应的每次查询缓存时会先检查key
是否有效,若失效,则重新从数据库加载,并标记key
为有效
对于被序列化的数据 ,我们在增删改的时候采用删除key
的方式进行懒加载,等待到下次查询时发现缓存不命中,自动从数据库加载打破缓存
缓存淘汰策略 部分较大的数据被设置了存活时间,过期自动淘汰,它们有:
dishListJsonByMerchant
orderListById