1.redis的客户端逻辑
1.1JedisCluster整合springboot完成
user用户系统使用redis
product使用redis
order/cart/seckill/search都可以使用
redis
封装成一个maven工程
代码满足所有的工程扫描范围(cn.tedu)
user/product等系统依赖这个maven工程
1.2user系统测试jedisCluster
8000 8001 8002 这三个节点信息创建spring管理的bean对象JedisCluster
set/get 方法
name—>8001 只要8004替换成功
key2–>8002 8005
key3—>8000 8003
1.3实现的jediscluster底层的原理
初始化过程创建的内存数据结构
set/get方法滤清对内存对象数据的使用逻辑
2.user最终版本代码
注入jedisCluster使用
2.1登录顶替
2.2超时突然断开
3.product的商品缓存和高并发锁
3.1商品缓存
接收请求,就收参数productId,创建key值(不同的商品时不同的redis的key值数据)product-query_+productId
到缓存redis获取数据
非空:缓存有数据不访问持久层
返回
空:缓存没数据(第一次使用缓存),到持久层获取,并且存储在缓存一份,供后续使用
3.2更新商品的高并发锁
查询缓存逻辑,如果更新时,对数据库做数据的更新造成的缓存数据不一致
4.cart购物车
4.1查询购物车
4.2新增购物车
4.3更新购物车num
4.4删除购物车
JedisCluster整合springboot
<parent>
<groupId>cn.tedu</groupId>
<artifactId>easymall-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!--springboot 传递spring内容 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
编写配置代码
配置类
package cn.tedu.common.redis.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ClusterConfig {
}
package cn.tedu.common.redis.config;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix="redis.cluster")
public class ClusterConfig {
//四个属性
private List<String> nodes;
private Integer maxTotal;
private Integer maxIdle;
private Integer minIdle;
public List<String> getNodes() {
return nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
public Integer getMaxTotal() {
return maxTotal;
}
public void setMaxTotal(Integer maxTotal) {
this.maxTotal = maxTotal;
}
public Integer getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(Integer maxIdle) {
this.maxIdle = maxIdle;
}
public Integer getMinIdle() {
return minIdle;
}
public void setMinIdle(Integer minIdle) {
this.minIdle = minIdle;
}
}
//初始化读取属性后创建的bean对象JedisCluster
@Bean
public JedisCluster initJedisCluster(){
//手集节点信息
Set<HostAndPort> set=new HashSet<HostAndPort>();
for(String node:nodes){
//每次循环拿到ip:port
String host=node.split(":")[0];
int port=Integer.parseInt(node.split(":")[1]);
set.add(new HostAndPort(host, port));
}
//配置对象
GenericObjectPoolConfig config=new GenericObjectPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
//创建JedisCluster
return new JedisCluster(set,config);
}
2.3其它工程依赖common-redis
业务系统依赖
product
user依赖
业务系统根据读取属性配置properties
user中实现一个测试代码
业务逻辑使用redis
1.user的业务逻辑
已经实现了user的登录功能,解决session共享的问题
1.1使用JedisCluster替换连接池
private JedisCluster jedis;
代码中的finally不需要了可以删除
1.2登录顶替
问题:一个用户,可以被多个客户端同时登录,同时具备一个用户的所有的权限
解决办法:实现登录顶替,一个用户在redis中只允许存一个ticket ticket-userJson 将ticket作为有效key值和userId userName唯一值绑定
扩展:一个账号三个人使用
注意:【更改了脚本,因为所删除的文件跑到了src目录下】
for port in {8000..8005};do ./redis-cli -c -p $port shutdown;done;
rm -rf dump800*;
rm -rf appendonly800*;
rm -rf nodes-800*;
for port in {8000..8005};do ./redis-server /usr/local/redis/bin/$port/redis-cluster.conf;done;
echo yes|./redis-trib.rb create --replicas 1 192.168.253.129:8000 192.168.253.129:8001 192.168.253.129:8002 192.168.253.129:8003 192.168.253.129:8004 192.168.253.129:8005;
/**
* 顶替逻辑:
* login=login_caoyang 存在吗?删除login_caoyang的值的值:不用管
* newTicket=caoyangtimeid jsonuser
* login=login_caoyang caoyangtimeid
*
*/
//判断登录的权限检验select where user_name and password
//加密
user.setUserPassword(MD5Util.md5(user.getUserPassword()));
User exist=userMapper.selectUserByUserNameAndPassword(user);
String loginKey="login_"+user.getUserName();
String newTicket="";
//判断loginKey是不是存在
if(jedis.exists(loginKey)){
//曾经有人登录过
//将oldTicket
String oldTicket=jedis.get(loginKey);
jedis.del(oldTicket);
}
//正常设置newTicket-userJson
try {
newTicket="EM_TICKET"+System.currentTimeMillis()+exist.getUserId();
//对象转化userJson
String userJson=MapperUtil.MP.writeValueAsString(exist);
jedis.setex(newTicket, 60*60*2, userJson);
//设置有效的ticket使用
jedis.setex(loginKey, 60*60*2, newTicket);
} catch (Exception e) {
e.printStackTrace();
return "";
}
return newTicket;
1.3突然超时
问题:如果超时设置2小时,用户访问超过2小时临界点突然断开
pttl是获取剩余的时间
pexpire是更改剩余的时间
//判断剩余的时间[增加续约]
Long leftTime=jedis.pttl(ticket);
if(leftTime<1000*60*30){//小于30分钟
//续约 续约1小时
jedis.pexpire(ticket, leftTime+1000*60*60);
}
return jedis.get(ticket);
2.商品系统
简单来说,就是在高并发情况下,查询的速度比更更新的速度快,导致查到的商品与数据库中已经更改的商品还是不一致,或者说有可能查询一直在等待更改操作,一直卡顿。
高并发情况下造成顺序错误
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>easymall-common-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
#redis-cluster
redis.cluster.nodes=192.168.253.129:8000,192.168.253.129:8001,192.168.253.129:8002
redis.cluster.maxTotal=200
redis.cluster.maxIdle=8
redis.cluster.minIdle=3
@Autowired
private JedisCluster cluster;
public Product queryPageProducts(String productId) {
//引入缓存判断更新锁的存在
//生成当前逻辑中需要的key
//锁的key product_update_productId+.lock
String updateLock="product_update_"+productId+".lock";
String productKey="product_"+productId;
//判断锁是否存在
try {
if(cluster.exists(updateLock)){
//锁存在,有人更新数据,缓存就不能使用
return productMapper.selectProductById(productId);
}else{
//锁不存在,判断缓存逻辑
if(cluster.exists(productKey)){
//缓存命中,解析productJson
//src就是json字符串
//value class 对象解析的类反射对象
return MapperUtil.MP.readValue(cluster.get(productKey), Product.class);
}else{
//查持久层
Product product=productMapper.selectProductById(productId);
//加入缓存
cluster.setex(productKey, 60*60*2,MapperUtil.MP.writeValueAsString(product));
return product;
}
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
//TODO 商品查询的缓存
//return productMapper.selectProductById(productId);
}
public void deployProduct(Product product) {
try {
//TODO新增缓存
//补齐数据 uuid保存productId
product.setProductId(UUID.randomUUID().toString());
//1030-dsaf-sdfa-saf-saf
productMapper.insertProduct(product);
//加入缓存
String productKey="product_"+product.getProductId();
cluster.setex(productKey, 60*60*2,MapperUtil.MP.writeValueAsString(product));
} catch (Exception e) {
e.printStackTrace();
}
}
public void renewProduct(Product product) {
/*
* 1.加锁(超时时间5分钟)
* 2.删除缓存productKey
* 3.更新数据库
* 4.释放锁
*/
String updateLock="product_update_"+product.getProductId()+".lock";
String productKey="product_"+product.getProductId();
cluster.setex(updateLock,60*5,"");
cluster.del(productKey);
productMapper.updateProductById(product);
cluster.del(updateLock);
}
2.4扩展redis使用的思路
需求:
注册登录时填写账号表单提交前的验证码发送的逻辑
前端限制发送频率,后端代码也要限制各种规则(5分钟最多发3次,超过3次非法,当前手机号锁定10分钟不能发送)
用redis存储发送的验证码
手机号.code
关键逻辑生成5分钟超过三次的逻辑,判断是否生成一个10分钟超时的锁
解决方案:
购物车系统
1.功能演示
1.1查询我的购物车
1.2新增购物车
1.3修改购物车num
1.4删除购物车
2.数据库表格的结构
t_cart
user-id/product_id:购物车表格的复合主键。两个字段同时存在定义一行数据的意义:某个用户想要购买某个商品
num:购买的数量
3.根据接口文件开发功能
3.1搭建一个购物车系统
user/product
eureka client注册在eureka使用
用到ribbon(新增购物车时,数据使用ribbon调用商品服务)
持久层
代码架子
启动类
package cn.tedu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@MapperScan("cn.tedu.cart.mapper")
@EnableEurekaClient
public class StarterCartService {
public static void main(String[] args) {
SpringApplication.run(StarterCartService.class, args);
}
//ribbon 创建一个可以负载均衡访问服务的对象
@Bean
@LoadBalanced
public RestTemplate initRestTemplate(){
return new RestTemplate();
}
}
代码架子
zuul网关添加cartservice服务的路由转发
zuul.routes.cart.path=/zuul-cart/**
zuul.routes.cart.serviceId=cartservice
3.3购物车新增
接口文件
controller
package cn.tedu.cart.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.jt.common.pojo.Cart;
import com.jt.common.vo.SysResult;
import cn.tedu.cart.service.CartService;
@RestController
@RequestMapping("cart/manage")
public class CartController {
@Autowired
private CartService cartService;
//查询我的购物车
@RequestMapping("query")
public List<Cart> queryMyCarts(String userId){
return cartService.queryMyCarts(userId);
}
//新增商品到我的购物车
@RequestMapping("save")
public SysResult saveMyCart(Cart cart){
//userd productId num
try {
cartService.saveMyCart(cart);
return SysResult.ok();
} catch (Exception e) {
e.printStackTrace();
return SysResult.build(201, "新增购物车失败", null);
}
}
//更新购物车num数量
@RequestMapping("update")
public SysResult updateMyCartNum(Cart cart){
try {
cartService.updateMyCartNum(cart);
return SysResult.ok();
} catch (Exception e) {
e.printStackTrace();
return SysResult.build(201, "更新失败", null);
}
}
//购物车的删除
@RequestMapping("delete")
public SysResult deleteMyCart(Cart cart){
try {
cartService.deleteMyCart(cart);
return SysResult.ok();
} catch (Exception e) {
e.printStackTrace();
return SysResult.build(201, "删除失败", null);
}
}
}
service
package cn.tedu.cart.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.jt.common.pojo.Cart;
import com.jt.common.pojo.Product;
import cn.tedu.cart.mapper.CartMapper;
@Service
public class CartService {
@Autowired
private CartMapper cartMapper;
public List<Cart> queryMyCarts(String userId) {
return cartMapper.selectCartsByUserId(userId);
}
@Autowired
private RestTemplate client;
public void saveMyCart(Cart cart) {
//查询已有
Cart exist=cartMapper.selectExistByUserIdAndProductId(cart);
if(exist!=null){
//已经存在 更新num的数量
//更行内存对象数据
cart.setNum(cart.getNum()+exist.getNum());
cartMapper.updateCartByUserIdAndProductId(cart);
}else{//没数据,新增购物车
//获取商品服务返回的数据,封装补齐cart对象
Product prod=client.getForObject("http://productservice/product/manage/item/"+cart.getProductId(),Product.class);
cart.setProductImage(prod.getProductImgurl());
cart.setProductName(prod.getProductName());
cart.setProductPrice(prod.getProductPrice());
//调用insert语句
cartMapper.insertCart(cart);
}
}
public void updateMyCartNum(Cart cart) {
cartMapper.updateCartByUserIdAndProductId(cart);
}
public void deleteMyCart(Cart cart) {
cartMapper.deleteCartByUserIdAndProductId(cart);
}
}
mapper
package cn.tedu.cart.mapper;
import java.util.List;
import com.jt.common.pojo.Cart;
public interface CartMapper {
List<Cart> selectCartsByUserId(String userId);
Cart selectExistByUserIdAndProductId(Cart cart);
void updateCartByUserIdAndProductId(Cart cart);
void insertCart(Cart cart);
void deleteCartByUserIdAndProductId(Cart cart);
}
mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.tedu.cart.mapper.CartMapper">
<select id="selectCartsByUserId" parameterType="String" resultType="Cart">
select * from t_cart where user_id=#{userId};
</select>
<select id="selectExistByUserIdAndProductId" parameterType="Cart" resultType="Cart">
select * from t_cart where user_id=#{userId} and product_id=#{productId};
</select>
<update id="updateCartByUserIdAndProductId" parameterType="Cart">
update t_cart set num=#{num} where user_id=#{userId} and product_id=#{productId};
</update>
<insert id="insertCart" parameterType="Cart">
insert into t_cart (user_id,product_id,product_price,product_image,product_name,num) values(#{userId},#{productId},#{productPrice},#{productImage},#{productName},#{num})
</insert>
<delete id="deleteCartByUserIdAndProductId" parameterType="Cart">
delete from t_cart where user_id=#{userId} and product_id=#{productId};
</delete>
</mapper>
单机的数据库策略
上图违反三范式
解决方式1为上图