2.整合redis

目标:整合redis实现分布式session存储

1. 添加依赖


<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>fastjsonartifactId>
    <version>1.2.38version>
dependency>


<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
dependency>

2. yml配置文件

redis:
    host: 127.0.0.1
    port: 6379
    max-idle: 5
    max-total: 10
    max-wait-millis: 3000

3. 读取这些配置

@Component
@Data
public class RedisConfig {
    /*****redis config start*******/
    @Value("${redis.host}")
    private String redisHost;
    @Value("${redis.port}")
    private int redisPort;
    @Value("${redis.max-idle}")
    private int redisMaxTotal;
    @Value("${redis.max-total}")
    private int redisMaxIdle;
    @Value("${redis.max-wait-millis}")
    private int redisMaxWaitMillis;
    /*****redis config end*******/
}

4. RedisPoolFactory构建redisPool

@Service
public class RedisPoolFactory {

    @Autowired
    RedisConfig redisConfig;

    @Bean
    public JedisPool JedisPoolFactory() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxIdle(redisConfig.getRedisMaxIdle());
        poolConfig.setMaxTotal(redisConfig.getRedisMaxTotal());
        poolConfig.setMaxWaitMillis(redisConfig.getRedisMaxWaitMillis());
        JedisPool jp = new JedisPool(poolConfig, redisConfig.getRedisHost(), redisConfig.getRedisPort());
        return jp;
    }

}

5. 在用redisPool进行操作之前,先解决一下key的生成问题

接口(定义契约)—-抽象类(通用方法)—-实现类(具体实现)

接口:接口定义两个方法声明,一个是获取key的前缀,一个是过期时间

public interface KeyPrefix {

    public int expireSeconds();

    public String getPrefix();

}

抽象类:

public abstract class BasePrefix implements KeyPrefix{

    private int expireSeconds;

    private String prefix;

    public BasePrefix(String prefix) {//0代表永不过期
        this(0, prefix);
    }

    public BasePrefix( int expireSeconds, String prefix) {
        this.expireSeconds = expireSeconds;
        this.prefix = prefix;
    }

    public int expireSeconds() {//默认0代表永不过期
        return expireSeconds;
    }

    public String getPrefix() {
        String className = getClass().getSimpleName();
        return className+":" + prefix;
    }

}

具体的实现类,这里先以MiaoshaUserKey为例:

public class MiaoshaUserKey extends BasePrefix{

    public static final int TOKEN_EXPIRE = 3600*24 * 2;
    private MiaoshaUserKey(int expireSeconds, String prefix) {
        super(expireSeconds, prefix);
    }
    public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
}

那么构造出来的prefix显然是MiaoshaUserKey:tk,超时时间也被传递进expireSeconds。

下面我们执行:

String token = UUIDUtil.uuid();
redisService.set(MiaoshaUserKey.token,token,user);

那么就相当于:

redisService.set("MiaoshaUserKey:tk",UUID,user对象);

那么,redis 的set方法具体是:

public  boolean set(KeyPrefix prefix, String key,  T value) {
    Jedis jedis = null;
    try {
        jedis =  jedisPool.getResource();
        String str = beanToString(value);//序列化成字符串
        if(str == null || str.length() <= 0) {
            return false;
        }
        //生成真正的key
        String realKey  = prefix.getPrefix() + key;//MiaoshaUserKey:tkUUID
        int seconds =  prefix.expireSeconds();//超时时间
        if(seconds <= 0) {
            jedis.set(realKey, str);
        }else {
            jedis.setex(realKey, seconds, str);//set进redis中
        }
        return true;
    }finally {
        returnToPool(jedis);
    }
}

再下一步是将UUID写到cookie中:

CookieUtil.writeLoginToken(response,token);

写入cookie:

public final static String COOKIE_NAME = "login_token";

public static void writeLoginToken(HttpServletResponse response, String token){
    Cookie ck = new Cookie(COOKIE_NAME,token);
    //ck.setDomain(COOKIE_DOMAIN);
    ck.setPath("/");//设值在根目录
    ck.setHttpOnly(true);//不允许通过脚本访问cookie,避免脚本攻击
    ck.setMaxAge(MiaoshaUserKey.token.expireSeconds());
    log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
    response.addCookie(ck);
}

这样,下面继续访问的时候,先根据cookie拿到UUID,再根据UUID从redis 中拿到User对象。

以浏览商品列表为例:

@RequestMapping("to_list")
public String toList(@CookieValue(value= CookieUtil.COOKIE_NAME,required = false) String cookieToken,
                   @RequestParam(value = CookieUtil.COOKIE_NAME,required = false) String paramToken,
                     Model model,HttpServletResponse response){
    if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
        return "login";
    }
    String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
    MiaoshaUser user = userService.getByToken(token,response);
    model.addAttribute("user",user);
    return "goods_list";
}

他是根据前面传来的token做下面的操作,当然还可以从后端读前面的cookie,取出相应的值。

其中:

MiaoshaUser user = userService.getByToken(token,response);

的具体实现是:

public MiaoshaUser getByToken(String token,HttpServletResponse response) {
    //先判断token是否为空
    if(StringUtils.isEmpty(token)){
        return null;
    }
    //根据token到redis中拿到相应的value
    MiaoshaUser user = redisService.get(MiaoshaUserKey.token,token,MiaoshaUser.class);
    redisService.set(MiaoshaUserKey.token,token,user);//key--->UserKey:tkUUID,value--->Serialized User
    //如果此时拿到user成功了,这里要重新设置一下redis过期时间
    if(user != null){
        redisService.set(MiaoshaUserKey.token,token,user);
    }
    return user;
}

注意:这里重新设置redis过期时间方式,在这里页面比较少的情况下,临时这样,但是在页面比较多的情况下,显然是不合适的,可以用一个过滤器,拦截所有的请求,然后在这个过滤器里进行登录过期时间的刷新。

6. 修改代码

我们发现,后面涉及到商品等其他的接口,按照这种写法,每次都要先获取cookie,然后从redis中获取user信息,获取成功,我们才能进行下一步操作。显然太过冗余,我们可以将其剥离出来,写在一个地方,避免冗余的代码。

我们的controller可以写成:

@RequestMapping("to_list")
public String toList(Model model,HttpServletResponse response,MiaoshaUser user){
    model.addAttribute("user",user);
    return "goods_list";
}

那么,我们在一个地方统一判断user是否能获取到。就要用到springmvc的机制了,我们可以试想springmvc支持的参数都是如何进来的呢?比如这里的MiaoShaUser是从什么地方注入进来的呢?

其实在UserArgumentResolver这个类中就可以拿到输入的参数,比如MiaoShaUser这个对象,然后再在resolveArgument这个方法里,对这个参数进行相应的处理:

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver{
    @Autowired
    private MiaoshaUserService userService;
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class clazz = parameter.getParameterType();
        return clazz== MiaoshaUser.class;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest webRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        String paramToken = request.getParameter(CookieUtil.COOKIE_NAME);
        String cookieToken = CookieUtil.readLoginToken(request);
        if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
            return "login";
        }
        String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
        return userService.getByToken(token,response);
    }
}

当然,这个对传入的参数进行修改的UserArgumentResolver要被重新加入进argumentResolvers中,相当于完成对原始的argumentResolvers中某个参数的重写:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
    @Autowired
    private UserArgumentResolver userArgumentResolver;

    @Override
    public void addArgumentResolvers(List argumentResolvers) {
        argumentResolvers.add(userArgumentResolver);
    }
}

这样,只要某个方法中传入了MiaoShaUser这个对象,那么就会进入resolveArgument()这个方法进行判断是否能拿到这个对象。

当然,我们可能更加常用的方式是springmvc拦截器来实现这个功能。并且在拦截器中,还可以实现更加复杂的逻辑,比如不仅可以判断user是否已经登陆,还可以针对特殊的url进行特别的处理。更加方便,在ssm电商项目中就是这样干的。

你可能感兴趣的:(秒杀,秒杀系统)