9-分布式缓存List-Hash最佳案例实战

225 阅读6分钟

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

本章主要讲解Redis数据结构里的List与Hash在实际使用场景做介绍与解答,分别对List-热销榜单实战演练,Hash-购物车的实战演练。通过这两个实战演练可以清晰了解到在实际应用开发的思路与应用。

在线教育-List-天热销榜单

需求分析

在淘宝京东的首页会有一个热销榜单,榜单又分为实时的与非实时的。而这个List结构适合做非实时的榜单。这种榜单的更新除了可以让程序自动更新外,有一些还需要人工运营替换榜单位置的。企业中的流程,定时任务计算昨天最多人访问的商品,晚上12点到1点更新到榜单上,预留一个接口支持人工运营。

为啥不是实时计算呢?其实在真正的高并发项目,都是预先计算好结果,然后直接返回数据,这样存储结构最简单。下面来简单讲解下,首先用户访问这个应用程序,应用程序再从Redis榜单里获取数据。但这个Redis榜单又是如何获取的呢?会有一个定时调度的任务+大数据处理的方法,这个方法先从数据库获取数据源再进行统计,统计完成后再给Redis中存储榜单。运营后台也可以人工的调动榜单。

在线教育-热销视频榜单实战

编写接口代码:在controller层中添加如下代码

@RequestMapping("rank")
 public JsonData videoRank(){
 List<VideoDO> list = redisTemplate.opsForList().range(RANK_KEY,0,-1);
 return JsonData.buildSuccess(list);
 }

接下来我们在测试环境里编写该接口的测试数据

@Test
 void saveRank(){
 String RANK_KEY = "rank:video";
 VideoDO video1 = new VideoDO(3,"PaaS⼯业级微服务⼤课","xdclass.net",1099);
 VideoDO video2 = new VideoDO(5,"AlibabaCloud全家桶实战","xdclass.net",59);
 VideoDO video3 = new VideoDO(53,"SpringBoot2.X+Vue3综合实战","xdclass.net",49);
 VideoDO video4 = new VideoDO(15,"玩转23种设计模式+最近实战","xdclass.net",49);
 VideoDO video5 = new VideoDO(45,"Nginx⽹关+LVS+KeepAlive","xdclass.net",89);
 
redisTemplate.opsForList().leftPushAll(RANK_KEY,video5,video4,video3,video2,video1);
 }

把代码写完后我们可以在浏览器发请求看下是否实现了榜单效果。

上面就实现了程序实现的榜单排行了,下面我们在继续模拟人工运营操作榜单的实现,再重新发送请求后,你会发现上面榜单第二名的"Alibaba全家桶实战"已经被替换掉,这实战例子就实现了人工替换榜单的教程。

/**
 * 替换榜单第⼆名
 */
 @Test
 void replaceRank(){
 String RANK_KEY = "rank:video";VideoDO video = new VideoDO(42,"⼩滴课堂⾯试专题第⼀季⾼级⼯程师","xdclass.net",89);
 //在集合的指定位置插⼊元素,如果指定位置已有元素,则覆盖,没有则新增
 
redisTemplate.opsForList().set(RANK_KEY,1,video);
 }

电商平台-Hash-购物车实战

前面我们讲了String和List数据类型对应的实战案例。能够充分明白在什么应用场景下用什么样的数据结构实现什么样的功能。接下来我们对Hash这个数据结构来讲解它一般用在什么样的功能上,如何实现的。

需求分析

在我们常用的电商购物平台,都具备有购物车这个功能。在购物车里,可以看到商品的图片、名字、价格、数量等等一些信息,并且可以购买不一样的商品。那如何去设计这个购物车呢?购物车常见的实现方式有:

(1)数据库存储:用户添加一条商品到购物车时就往对应的数据库表添加一条信息,后续可以根据商品的id来查询相对应的商品,这种方式存在着一定的性能瓶颈。

(2)前端本地存储(localstorage-sessionstorage ):localstorage在浏览器中存储是以key-value进行存储的,没有过期时间。sessionstorage 在浏览器中存储key-value,在关闭会话窗口后会删除这些数据。

(3)后端存储到缓存中,比如Redis:可以开启Redis的数据持久化AOP防止重启数据丢失。

购物车数据结构介绍

一个购物车里面会存在着多个购物项,所以购物车的结构是双层的Map结构:Map<String,Map<String,String>>,第一层Map的key是用来存储用户id,第二层Map的key是用来存储购物车中商品的id,Value是购物车中的数据。那么在Redis里就可以使用Hash结构用来实现。

电商购物车实战

VideoDO类前面第七章的内容已经编写过了,这里就不在给代码了

CartItemVO新建一个Vo层

public class CartItemVO {
 /**
 * 商品id
 */
 private Integer productId;
 /**
 * 购买数量
 */
 private Integer buyNum;
 /**
 * 商品标题
 */
 private String productTitle;
 /**
 * 图⽚
 */
 private String productImg;
 /**
 * 商品单价
 */
 private int price ;
 /**
 * 总价格,单价+数量
 */
 private int totalPrice;
 public int getProductId() {
 return productId;
 }
 public void setProductId(int productId) {
 this.productId = productId;
 }
 public Integer getBuyNum() {
 return buyNum;
 }
 public void setBuyNum(Integer buyNum) {
 this.buyNum = buyNum;
 }
 public String getProductTitle() {
 return productTitle;
 }
 public void setProductTitle(String
productTitle) {
 this.productTitle = productTitle;
 }
 public String getProductImg() {
 return productImg;
 }
 public void setProductImg(String
productImg) {
 this.productImg = productImg;
 }
 /**
 * 商品单价 * 购买数量
 * @return
 */
 public int getTotalPrice() {
 return this.price*this.buyNum;
 }
 public int getPrice() {
 return price;
 }
 public void setPrice(int price) {
 this.price = price;
 }
 public void setTotalPrice(int totalPrice) {
 this.totalPrice = totalPrice;
 }
}

CartVO在VO层里

public class CartVO {
 /**
 * 购物项
 */
 private List<CartItemVO> cartItems;
 /**
 * 购物⻋总价格
 */
 private Integer totalAmount;
 /**
 * 总价格
 * @return
 */
 public int getTotalAmount() {
 return
cartItems.stream().mapToInt(CartItemVO::getTotalPrice).sum();
 }
 public List<CartItemVO> getCartItems() {
 return cartItems;
 }
 public void setCartItems(List<CartItemVO>cartItems) {
 this.cartItems = cartItems;
 }
}

数据源层在Dao层创建,这里是数据库里的数据。

@Repository
public class VideoDao {
 private static Map<Integer,VideoDO> map = new HashMap<>();
 static {
 map.put(1,new VideoDO(1,"⼯业级PaaS云平台+SpringCloudAlibaba 综合项⽬实战(完 结)","https://xdclass.net",1099));
 map.put(2,new VideoDO(2,"玩转新版⾼性能RabbitMQ容器化分布式集群实战","https://xdclass.net",79));
 map.put(3,new VideoDO(3,"新版后端提效神器MybatisPlus+SwaggerUI3.X+Lombok","https://xdclass.net",49));
 map.put(4,new VideoDO(4,"玩转Nginx分布式架构实战教程 零基础到⾼级","https://xdclass.net",49));
 map.put(5,new VideoDO(5,"ssm新版SpringBoot2.3/spring5/mybatis3","https://xdclass.net",49));
 map.put(6,new VideoDO(6,"新⼀代微服务全家桶AlibabaCloud+SpringCloud实 战","https://xdclass.net",59));
 }
 /**
 * 模拟从数据库找
 * @param videoId
 * @return
 */
 public VideoDO findDetailById(int videoId)
{
 return map.get(videoId);
 }
}

编写一个Json工具类,用于存到Redis里做个Json的序列化方便操作

public class JsonUtil {
 // 定义jackson对象
 private static final ObjectMapper MAPPER = new ObjectMapper();
 /**
 * 将对象转换成json字符串。
 
 * @return
 */
 public static String objectToJson(Object data) {
 try {
 String string = MAPPER.writeValueAsString(data);
 return string;
 } catch (Exception e) {
 e.printStackTrace();
 }
 return null;
 }
 /**
 * 将json结果集转化为对象
 *
 * @param jsonData json数据
 * @param clazz 对象中的object类型
 * @return
 */
 public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
 try {
 T t = MAPPER.readValue(jsonData,beanType);
 return t;
 } catch (Exception e) {
 e.printStackTrace();
 }
 return null;
 }
}

购物车接口开发CartController,在controller层创建。

@RestController
@RequestMapping("api/v1/cart")
public class CartController{
 
 @RequestMapping("addCart")
 public JsonData addCart(int videoId,intbuyNum){
 //获取购物⻋
 BoundHashOperations<String, Object,Object> myCart = getMyCartOps();
 Object cacheObj =myCart.get(videoId+"");
 String result = "";
 if (cacheObj != null) {
 result = (String) cacheObj;
 }
 if (cacheObj == null) {
 //不存在则新建⼀个购物项
 CartItemVO cartItem = new CartItemVO();
 //从数据库查询详情,我们这边直接随机写个
 VideoDO videoDO = videoDao.findDetailById(videoId);
 videoDO.setId(videoId);
 
cartItem.setPrice(videoDO.getPrice());
 cartItem.setBuyNum(buyNum);
 cartItem.setProductId(videoId);
 
cartItem.setProductImg(videoDO.getImg());
 
cartItem.setProductTitle(videoDO.getTitle());
 myCart.put(videoId+"",JsonUtil.objectToJson(cartItem));
 } else {
 //存在则新增数量
 CartItemVO cartItem = JsonUtil.jsonToPojo(result, CartItemVO.class);
 
cartItem.setBuyNum(cartItem.getBuyNum() +buyNum);
 myCart.put(videoId+"",JsonUtil.objectToJson(cartItem));
 }
 return JsonData.buildSuccess();
 }
 }

编写购物车通用方法

/**
 * 抽取我的购物⻋通⽤⽅法
 *
 * @return
 */
 private BoundHashOperations<String, Object,Object> getMyCartOps() {
 String cartKey = getCartKey();
 return redisTemplate.boundHashOps(cartKey);
 }
 /**
 * 获取购物⻋的key
 *
 * @return
 */
 private String getCartKey() {
 //从拦截器获取 ,这⾥写死即可,每个⽤户不⼀样
 int userId = 88;
 String cartKey =
String.format("product:cart:%s", userId);
 return cartKey;
 }

在模拟完购物车功能实现后,可以接着来实现查看和清空购物车的功能。在CartController里继续编写

 @GetMapping("/mycart")
 public JsonData findMyCart(){
 
BoundHashOperations<String,Object,Object>
myCart = getMyCartOps();
 List<Object> itemList =
myCart.values();
 List<CartItemVO> cartItemVOList = new
ArrayList<>();
 for(Object item: itemList){
 CartItemVO cartItemVO =
JsonUtil.jsonToPojo((String)item,CartItemVO.cla
ss);
 cartItemVOList.add(cartItemVO);
 }
 //封装成cartvo
 CartVO cartVO = new CartVO();
 cartVO.setCartItems(cartItemVOList);
 return JsonData.buildSuccess(cartVO);
 }
//清空购物车
 @GetMapping("/clear")
public JsonData clear() {
 String cartKey = getCartKey();
 redisTemplate.delete(cartKey);
 return JsonData.buildSuccess();
}

到这里本章已经对List-Hash这两个数据结构的实战环节已经结束了,总结一下List可以实现榜单的功能,Hash可以实现购物车的功能。

本章小结:

通过本章的两个实战演练,可以充分了解到List与Hash在Redis中的使用方法,并且实现在当前热门的功能上,当然这些数据结构还有很多的应用场景,需要小伙伴们自行学习了。