4.String数据结构实战
4.1.图形验证码存入Redis实战
- 背景:
- 注册-登录-修改密码一版需要发送验证码,但是容易被攻击而已调用。
- 如何避免自己的网站被刷呢
- 增加图形验证码
- 单IP请求次数限制
- 限制号码发送
(1)Kaptcha框架介绍
- 验证码的字体/大小/颜色
- 验证码内容的范围(数字、字母、中文汉字)
- 验证码图片的大小,边框,边框粗细,边框颜色
- 验证码的干扰线,验证码的样式
(2)添加Kaptcha依赖
<!--kaptcha依赖包 (图形验证码)--> <dependency> <groupId>com.baomidou</groupId> <artifactId>kaptcha-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>
(3)代码配置,编写CaptchaConfig类
@Configuration public class CaptchaConfig { /** * 验证码配置 * Kaptcha配置类名 * * @return */ @Bean @Qualifier("captchaProducer") public DefaultKaptcha kaptcha() { DefaultKaptcha kaptcha = new DefaultKaptcha(); Properties properties = new Properties(); //验证码个数 properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); //字体间隔 properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,"8"); //干扰线颜色 //干扰实现类 properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); //图片样式 properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple"); //文字来源 properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789"); Config config = new Config(properties); kaptcha.setConfig(config); return kaptcha; } }
(4)编写统一返回工具类
public class JsonData { /** * 状态码 0 表示成功 */ private Integer code; /** * 数据 */ private Object data; /** * 描述 */ private String msg; public JsonData(int code,Object data,String msg){ this.data = data; this.msg = msg; this.code = code; } /** * 成功,不传入数据 * @return */ public static JsonData buildSuccess() { return new JsonData(0, null, null); } /** * 成功,传入数据 * @param data * @return */ public static JsonData buildSuccess(Object data) { return new JsonData(0, data, null); } /** * 失败,传入描述信息 * @param msg * @return */ public static JsonData buildError(String msg) { return new JsonData(-1, null, msg); } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } } z
(5)编写CommonUtil工具类(获取前台请求ip和md5方法)
public class CommonUtil { /** * 获取ip * @param request * @return */ public static String getIpAddr(HttpServletRequest request) { String ipAddress = null; try { ipAddress = request.getHeader("x-forwarded-for"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if (ipAddress.equals("127.0.0.1")) { // 根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ipAddress = inet.getHostAddress(); } } // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length() // = 15 if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } } } catch (Exception e) { ipAddress=""; } return ipAddress; } public static String MD5(String data) { try { java.security.MessageDigest md = MessageDigest.getInstance("MD5"); byte[] array = md.digest(data.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte item : array) { sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); } return sb.toString().toUpperCase(); } catch (Exception exception) { } return null; } }
(6)编写生成验证码存入Redis的逻辑
@RestController @RequestMapping("/api/v1/captcha") public class CaptchaController{ @Autowired private StringRedisTemplate redisTemplate; @Autowired private Producer captchaProduct; @GetMapping("/get_captcha") public void getCaptcha(HttpServletRequest request,HttpServletResponse response){ /** * 获取随机的验证码 */ String captchaProducerText = captchaProducer.createText(); String key = getCaptchaKey(request); //放在Redis10分钟过期 redisTemplate.opsForValue().set(key,captchaProducerText,10, TimeUnit.MINUTES); BufferedImage image = captchaProducer.createImage(captchaProducerText); ServletOutputStream outputStream = null; try{ outputStream = response.getOutputStream(); ImageIO.write(image,"jpg",outputStream); outputStream.flush(); outputStream.close(); }catch (Exception e){ e.printStackTrace(); } } @GetMapping("/send_code") public JsonData sendCode(@RequestParam(value = "to",required = true)String to, @RequestParam(value = "captcha",required = true) String captcha, HttpServletRequest request){ String key = getCaptchaKey(request); String cacheCaptcha = redisTemplate.opsForValue().get(key); if(cacheCaptcha != null && captcha != null && cacheCaptcha.equalsIgnoreCase(captcha)){ //匹配通过一定要删除当前key redisTemplate.delete(key); //TODO 发送验证码逻辑 return JsonData.buildSuccess(); }else{ return JsonData.buildError("图形验证码不正确"); } } /* * * 获取存在缓存中的key用请求的ip和请求头,md5加密 */ private String getCaptchaKey(HttpServletRequest request){ String ip = CommonUtil.getIpAddr(request); String userAgent = request.getHeader("User-Agent"); String key = "user-service:captcha:"+CommonUtil.MD5(ip+userAgent); return key; } }
4.2.高并发商品首页热点数据开发实战
(1)热点数据
- 经常会被查询,但是不经产被修改或者删除的数据
- 首页-详情页
(2)链路逻辑
- 检查缓存是否存在
- 缓存不存在则查询数据库
- 查询数据库的结果放到缓存中,设置过期时间
- 下次访问则命中缓存
(3)接口开发
- 实体类编写,商品项,商品卡片
//商品卡片实体类,里面有多个商品 public class VideoCardDO { private String title; private int id; private int weight; List<VideoDO> videoDOList; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public List<VideoDO> getVideoDOList() { return videoDOList; } public void setVideoDOList(List<VideoDO> videoDOList) { this.videoDOList = videoDOList; } } //商品实体类 public class VideoDO { private int id; private String title; private String img; private int price; public VideoDO() { } public VideoDO(String title, String img, int price) { this.title = title; this.img = img; this.price = price; } public VideoDO(int id, String title, String img, int price) { this.id = id; this.title = title; this.img = img; this.price = price; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getImg() { return img; } public void setImg(String img) { this.img = img; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } } c
- Dao层编写,这块采用模拟数据库查询
@Repository public class VideoCardDao { public List<VideoCardDO> list(){ try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } List<VideoCardDO> cardDOList = new ArrayList<>(); VideoCardDO videoCardDO1 = new VideoCardDO(); VideoDO videoDO1 = new VideoDO(1,"SpringCloud","xxxxxxxxxxxxx",1000); VideoDO videoDO2 = new VideoDO(2,"Netty","xxxxxxxxxxxxx",234); VideoDO videoDO3 = new VideoDO(3,"面试专题视频","xxxxxxxxxxxxx",3564); VideoDO videoDO4 = new VideoDO(4,"AlibabaCloud","xxxxxxxxxxxxx",123); VideoDO videoDO5 = new VideoDO(5,"Dubbo","xxxxxxxxxxxxx",445); videoCardDO1.setId(1); videoCardDO1.setTitle("热门视频"); List<VideoDO> videoDOS = new ArrayList<>(); videoDOS.add(videoDO1); videoDOS.add(videoDO2); videoDOS.add(videoDO3); videoDOS.add(videoDO4); videoDOS.add(videoDO5); videoCardDO1.setVideoDOList(videoDOS); cardDOList.add(videoCardDO1); VideoCardDO videoCardDO2 = new VideoCardDO(); VideoDO videoDO6 = new VideoDO(1,"SpringCloud","xxxxxxxxxxxxx",1000); VideoDO videoDO7 = new VideoDO(2,"Netty","xxxxxxxxxxxxx",234); VideoDO videoDO8 = new VideoDO(3,"面试专题视频","xxxxxxxxxxxxx",3564); VideoDO videoDO9 = new VideoDO(4,"AlibabaCloud","xxxxxxxxxxxxx",123); VideoDO videoDO10 = new VideoDO(5,"Dubbo","xxxxxxxxxxxxx",445); videoCardDO1.setId(1); videoCardDO1.setTitle("项目实战"); List<VideoDO> videoDOS2 = new ArrayList<>(); videoDOS2.add(videoDO6); videoDOS2.add(videoDO7); videoDOS2.add(videoDO8); videoDOS2.add(videoDO9); videoDOS2.add(videoDO10); videoCardDO2.setVideoDOList(videoDOS2); cardDOList.add(videoCardDO2); return cardDOList; } }
- service层编写
public interface VideoCardService { List<VideoCardDO> list(); } @Service public class VideoCardServiceImpl implements VideoCardService { @Autowired private VideoCardDao videoCardDao; @Autowired private RedisTemplate redisTemplate; private static final String VIDEO_CARD_CACHE_KEY = "video:card:key"; @Override public List<VideoCardDO> list() { Object cacheObj = redisTemplate.opsForValue().get(VIDEO_CARD_CACHE_KEY); if(cacheObj != null){ return (List<VideoCardDO>)cacheObj; }else{ List<VideoCardDO> list = videoCardDao.list(); redisTemplate.opsForValue().set(VIDEO_CARD_CACHE_KEY,list,10,TimeUtil.MINUTES); return list; } } }
- Controller层
@Autowired private VideoCardService videoCardService; /** * 缓存查找热点卡片 * @return */ @RequestMapping("/list_cache") public JsonData listCache(){ List<VideoCardDO> list = videoCardService.list(); return JsonData.buildSuccess(list); }
4.3.Redis6+Lua脚本实现原生分布式锁
(1)分布式锁简介
- 简介:分布试锁核心知识介绍和注意事项
- 背景:保证同一时间只有一个客户端可以对共享资源进行操作
- 案例:优惠劵领券限制次数、商品库存超卖
- 核心:
- 为了防止分布式系统中的多个线程之间进行相互干扰,我们需要一种分布式协调技术来对这些进程进行调度
- 利用互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
- 避免共享资源并发操作导致数据问题
- 加锁:
- 本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
- 分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程公用锁标记,可以用Redis、Zookeeper、MySql等都可以
- 设计分布式锁应该考虑的东西
- 排他性:在分布式应用集群中,同一个方法在同一时间只能被一台机器的一个线程执行
- 容错性:分布式锁一定能得到释放,比如客户端崩溃或者网络中断
- 满足可重入、高性能、高可用
- 注意分布式锁的开销、锁粒度
(2)基于Redis实现分布式锁的几种坑
- 实现分布式锁可以用Redis、Zookeeper、MySql数据库这几种,性能最好的是Redis且最容易理解的
- 分布式锁离不开key -value 设置
key是锁的唯一标识,一版按业务来决定命名,比如想要给一种优惠劵活动加锁,key命名为"coupon:id",value可以用固定值,比如设置成1
- 基于redis实现分布式锁,文档:http://www.redis.cn/commands.html#string
- 加锁setnx key value
setnx 的含义就是SET if Not Exists,有两个参数setnx(key,value),该方法是原子性操作。 如果key不存在,则设置当前key成功,返回1。 如果当前key已经存在,则设置当前key失败,返回0。
- 解锁del(key)
得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用del(key)
- 配置锁超时expire(key,30s)
客户端崩溃或者网络中断,资源将永会被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显示释放,这把锁也要在一定时间后自动释放。
- 综合伪代码
methodA(){ String key = "coupon_66" if(setnx(key,1) == 1){ //注意设置时间和设置key不是原子性 expire(key,30,TimeUnit.MILLISECONDS) try{ //做对应的业务逻辑 //查询用户是否已经领卷 }finally{ del(key) } }else{ //睡眠100毫秒,然后自旋调用本方法 methodA() } }
- 存在什么问题
多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,但是expire失败,且死机,则就是个死锁。
使用原子性命令:设置和配置过期时间 setnx|setex 如:set key 1 ex 30 nx redisTemplate.opsFosValue().setIfAbsent("seckill_1",success,30,TimeUnit.MILLISECONDS)
业务超时,存在其他线程误删,key30秒过期,假如线程A执行很慢超过30s,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没有执行完成,结果A把B加的锁给删掉了。
- 进一步细化误删
可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁,那value应该是存当前线程的标识或者uuid methodA(){ String key = "coupon_66" if(setnx(key,1) == 1){ //注意设置时间和设置key不是原子性 expire(key,30,TimeUnit.MILLISECONDS) try{ //做对应的业务逻辑 //查询用户是否已经领卷 }finally{ //删除锁操作判断是否为当前线程加的 if(redisTemplate.get(key).equals(value)){ //还在当前时间规定内 del(key) } } }else{ //睡眠100毫秒,然后自旋调用本方法 methodA() } }
- 核心还是判断和删除命令不是原子性操作导致的
- 总结
- 加锁+配置过期时间:保证原子性操作
- 解锁:防止误删除、也要保证原子性操作
- 采用Lua脚本+redis,保证多个命令的原子性
(3)Lua脚本+Redis实现分布式锁的编码实现
//获取lock的值和传递的值⼀样,调⽤删除操作返回1,否则返回0 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; //Arrays.asList(lockKey)是key列表,uuid是参数 Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class),Arrays.asList(lockKey), uuid);
@Slf4j @RestController @RequestMapping("/api/v1/coupon") public class CouponController { @Autowired private RedisTemplate redisTemplate; @GetMapping("/add") public JsonData save(@RequestParam(value = "coupon_id",required = true) int couponId){ //防止其他线程误删 String uuid = UUID.randomUUID().toString(); String lockKey = "lock:coupon:"+couponId; lock(couponId,uuid,lockKey); return JsonData.buildSuccess(); } private void lock(int couponId,String uuid,String lockKey) { Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, Duration.ofSeconds(30)); log.info(uuid+"---加锁状态:"+nativeLock); //定义Lua脚本 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; if(nativeLock){ //加锁成功,做相应的业务逻辑 try{ //核心业务逻辑 TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } finally { //解锁操作 Object result = redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(lockKey), uuid); log.info("解锁结果:"+result); } }else{ //加锁失败进入睡眠5s,然后在自旋调用 try { log.info("加锁失败,睡眠5s,进入自旋"); TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } lock(couponId,uuid,lockKey); } } }
setIfAbsent(): execute():
5.List数据结构实战
5.1.昨日热销榜单实战
(1)需求:
- 天猫每天的热销榜单,每天更新一次
- 需要支持人工运营替换榜单的位置
- (2)企业中流程:
- 定时任务计算昨天那些商品出售的数量最多
- 晚上12点到1点更新到榜单上
- 预留一个接口,支持人工运营
- (3)类似场景:
- 京东:热销手机榜单、电脑榜单等
- 百度:搜索热榜
(4)编码实战
- 开发接口
@RequestMapping("rank") public JsonData videoRank(){ List<VideoDO> list = redisTemplate.opsForValue().range(RANK_KEY,0,-1); return JsonData.buildSuccess(list); }
- 测试数据
@Test public void rankTest(){ 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); //leftPushAll向左边插入,所以放在最后一位的才是首个 redisTemplate.opsForList().leftPush(RANK_KEY,video6,video5,video4,video3,video2,video1); //rightPushAll向右边插入,所以首个就是第一个 //sTemplate.opsForList().leftPush(RANK_KEY,video1,video2,video3,video4,video5); }
6.Hash数据结构实战
6.1.购物车实现案例实战
(1)背景:
- 电商购物车实现,支持买多见商品,每个商品不同数量
- 支持高性能处理
- (2)购物车常见的实现方式:
- 实现方式一:存储到数据库
- 性能存在瓶颈
- 实现方式二:前端本地存储-localstorage,sessionstorage
- localstorage在浏览器中存储key/value对,没有过期时间
- sessionstorage在浏览器中存储key/value对,在关闭会话窗口后将会删除这些数据
- 实现方式三:后端存储到缓存redis
- 可以开启AOF持久化防止重启丢失(推荐)
(2)购物车数据结构介绍
- 一个购物车里面,存在多个购物项
- 所以购物车是一个双层的Map
- Map<String,Map<String,String>>
- 第一层Map,key是用户id
- 第二层Map,key是购物车商品的id,值是购物项的数据
(3)对应redis里面的存储
(4)编码实战
- 实体类VideoDO、CartItemVO、CartVO
public class CartItemVO { /** * 商品id */ private Integer productId; /** * 购买数量 */ private Integer buyNum; /** * 商品标题 */ private String productTitle; /** * 图片 */ private String productImg; /** * 商品单价 */ private int price; /** * 总价格 */ private int totalPrice; public Integer getProductId() { return productId; } public void setProductId(Integer 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; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public int getTotalPrice() { return totalPrice*buyNum; } public void setTotalPrice(int totalPrice) { this.totalPrice = totalPrice; } }
public class CartVO { private List<CartItemVO> cartItemVOS; private Integer totalAmount; public List<CartItemVO> getCartItemVOS() { return cartItemVOS; } public void setCartItemVOS(List<CartItemVO> cartItemVOS) { this.cartItemVOS = cartItemVOS; } /** * 返回购物车总价格 * @return */ public Integer getTotalAmount() { //jdk8新语法 return cartItemVOS.stream().mapToInt(CartItemVO::getTotalPrice).sum(); } public void setTotalAmount(Integer totalAmount) { this.totalAmount = totalAmount; } }
- 模拟dao层,数据库根据id返回数据
@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 findByVideoId(int videoId){ return map.get(videoId); } }
- JsonUtil工具类开发
public class JsonUtil { private static final ObjectMapper MAPPER = new ObjectMapper(); /** * 对象转json字符串的方法 * @param data * @return */ public static String objectToJson(Object data){ try{ return MAPPER.writeValueAsString(data); } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } /** * json字符串转对象的方法 * @param jsonData * @param beanType * @param <T> * @return */ public static <T> T jsonToObject(String jsonData,Class<T> beanType){ try { T t = MAPPER.readValue(jsonData, beanType); return t; } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } }
- 开发VideoCardController,购物车控制层
@RestController @RequestMapping("/api/v1/cart") @Slf4j public class VideoCardController{ @Autowired private VideoDao videoDao; @Autowired private RedisTemplate redisTemplate; /** * 添加到购物车 * @param videoId * @param buyNum * @return */ @RequestMapping("/add") public JsonData addCart(int videoId,int buyNum){ /** * 获取购物车 */ BoundHashOperations<String, Object, Object> myCartOps = getMyCartOps(); Object cacheObj = myCartOps.get(videoId + ""); String result = ""; //当购物车有这个商品,转化成字符串 if(cacheObj != null){ result = (String) cacheObj; } if(cacheObj == null){ //购物车没这个商品,从数据库里拿出来,在放到缓存中 CartItemVO cartItemVO = new CartItemVO(); VideoDO videoDO = videoDao.findByVideoId(videoId); cartItemVO.setBuyNum(buyNum); cartItemVO.setPrice(videoDO.getPrice()); cartItemVO.setProductId(videoDO.getId()); cartItemVO.setProductImg(videoDO.getImg()); cartItemVO.setProductTitle(videoDO.getTitle()); cartItemVO.setTotalPrice(videoDO.getPrice()*buyNum); myCartOps.put(videoId+"", JsonUtil.objectToJson(cartItemVO)); }else{ //不为空就将字符串转成对象,增加商品购买数量,在转成字符串放到redis里 CartItemVO cartItemVO = JsonUtil.jsonToObject(result, CartItemVO.class); cartItemVO.setBuyNum(cartItemVO.getBuyNum()+buyNum); myCartOps.put(videoId+"",JsonUtil.objectToJson(cartItemVO)); } return JsonData.buildSuccess(); } /** * 查看我的购物车 * @return */ @RequestMapping("/my-cart") public JsonData getMyCart(){ //获取购物车 BoundHashOperations<String, Object, Object> myCartOps = getMyCartOps(); List<CartItemVO> cartItemVOS = new ArrayList<>(); List<Object> itemList = myCartOps.values(); for (Object item : itemList) { CartItemVO cartItemVO = JsonUtil.jsonToObject((String) item, CartItemVO.class); cartItemVOS.add(cartItemVO); } CartVO cartVO = new CartVO(); cartVO.setCartItemVOS(cartItemVOS); return JsonData.buildSuccess(cartVO); } /** * 清空我的购物车 * @return */ @RequestMapping("/clear") public JsonData clear(){ String cartKey = getCartKey(); redisTemplate.delete(cartKey); return JsonData.buildSuccess(); } /*******************通用的方法,获取购物购物车数据,获取当前key*****************/ /** * 获取我的购物车通用方法 * @return */ private BoundHashOperations<String,Object,Object> getMyCartOps(){ //获取定义在Hash里的key,指定方法拼接 String key = getCartKey(); //返回当前key的集合,没有则新建返回 return redisTemplate.boundHashOps(key); } /** * 获取购物车的key,用前缀加上用户的id * @return */ private String getCartKey(){ //用户id,获取用户id,JWT解密后获取 int userId = 88; String cartKey = String.format("video:cart:%s", userId); return cartKey; } }
7.Set数据结构实战
7.1.大数据下的用户画像标签去重
(1)简介
- 用户画像 英文User Profile,是根据用户基本属性、社会属性、行为属性、心理属性等真实信息抽象出的一个标签化的、虚拟的用户模型。“用户画像”的实质是对“人”的数字化。
- 应用场景很多,比如个性化推荐、精准营销、金融风控、精细化运营等等,举个例子来理解用户画像的实际实用价值,我们经常用手机网购,淘宝里面的千人千面,通过“标签tag”来对用户的多维度特征进行提炼和标识,那灭个人的用户画像就需要存储,set集合就适合去重。
- 用户画像不止针对某个人,也可以某一人群或行业的画像。
(2)案例
/** * 用户画像去重 */ @Test public void userProfile(){ BoundSetOperations operations = redisTemplate.boundSetOps("user:tags:1"); operations.add("car","student","rich","dog","guangdong","rich"); Set<String> set1 = operations.members(); System.out.println(set1); operations.remove("dog"); Set<String> set2 = operations.members(); System.out.println(set2); }
7.2.关注、粉丝、共同好友
(1)背景
- 社交应用里面的关注、粉丝、共同好友案例
(2)案例
public void testSet(){ BoundSetOperations operationsLW = redisTemplate.boundSetOps("user:lw"); operationsLW.add("A","B","C","D","E"); System.out.println("LW的粉丝:"+operationsLW.members()); BoundSetOperations operationsLX = redisTemplate.boundSetOps("user:lx"); operationsLX.add("A","B","F","Z","H"); System.out.println("LX的粉丝:"+operationsLX.members()); //差集 Set lwSet = operationsLW.diff("user:lx"); System.out.println("lw的专属用户:"+lwSet); Set lxSet = operationsLX.diff("user:lw"); System.out.println("lx的专属用户:"+lxSet); //交集 Set intersectSet = operationsLW.intersect("user:lx"); System.out.println("同时关注的用户:"+intersectSet); Set union = operationsLW.union("user:lx"); //并集 System.out.println("两个人的并集:"+union); Boolean a = operationsLW.isMember("A"); System.out.println("用户A是否为lw的粉丝:"+a); }
8.SortedSet数据结构实战
8.1.用户积分实时榜单
(1)背景
- 用户玩游戏-积分实时榜单
- IT视频热销实时榜单
- 电商商品热销实时榜单
- 一般的排行榜读多写少,可以对master进行写入操作做,然后多个slave进行读操作
(2)对象准备
public class UserPointVO { private String username; private String phone; public UserPointVO(String username, String phone) { this.username = username; this.phone = phone; } public UserPointVO() { } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserPointVO that = (UserPointVO) o; return Objects.equals(phone, that.phone); } @Override public int hashCode() { return Objects.hash(phone); } }
@Test public void testData(){ UserPointVO p1 = new UserPointVO("老王","13113"); UserPointVO p2 = new UserPointVO("老A","324"); UserPointVO p3 = new UserPointVO("老B","242"); UserPointVO p4 = new UserPointVO("老C","542345"); UserPointVO p5 = new UserPointVO("老D","235"); UserPointVO p6 = new UserPointVO("老E","1245"); UserPointVO p7 = new UserPointVO("老F","2356432"); UserPointVO p8 = new UserPointVO("老G","532332"); BoundZSetOperations boundZSetOperations = redisTemplate.boundZSetOps("point:rank:real"); boundZSetOperations.add(p1,348); boundZSetOperations.add(p2,18); boundZSetOperations.add(p3,328); boundZSetOperations.add(p4,848); boundZSetOperations.add(p5,98); boundZSetOperations.add(p6,188); boundZSetOperations.add(p7,838); boundZSetOperations.add(p8,8828); }
(3)接口开发
- 返回榜单-从大到小排序
/** * 返回全部榜单从大到小 * @return */ @RequestMapping("/real-rank2") public JsonData rankList2(){ Set set = redisTemplate.boundZSetOps("point:rank:real").reverseRange(0, -1); return JsonData.buildSuccess(set); }
- 返回榜单-从小到大排序
/** * 返回全部榜单从小到大 * @return */ @RequestMapping("/real-rank1") public JsonData rankList1(){ Set range = redisTemplate.boundZSetOps("point:rank:real").range(0, -1); return JsonData.buildSuccess(range); }
- 查询个人用户排名
/** * 查询个人用户排名 * @param username * @param phone * @return */ @RequestMapping("find_my_rank") public JsonData find(String username,String phone){ UserPointVO userPointVO = new UserPointVO(username,phone); Long rank = redisTemplate.boundZSetOps("point:rank:real").reverseRank(userPointVO); return JsonData.buildSuccess(++rank); }
- 查看个人积分
/** * 查看个人积分 * @param username * @param phone * @return */ @RequestMapping("find_my_score") public JsonData findMyScore(String username,String phone){ UserPointVO userPointVO = new UserPointVO(username,phone); Double score = redisTemplate.boundZSetOps("point:rank:real").score(userPointVO); return JsonData.buildSuccess(score); }
- 个人加积分
/** * 加积分 * @param username * @param phone * @return */ @RequestMapping("add_score") public JsonData addScore(String username,String phone){ UserPointVO userPointVO = new UserPointVO(username,phone); redisTemplate.boundZSetOps("point:rank:real").incrementScore(userPointVO,1000000); return JsonData.buildSuccess(redisTemplate.boundZSetOps("point:rank:real").reverseRange(0,-1)); }