百味皆苦 java后端开发攻城狮

MK电商项目

2019-07-25
百味皆苦

数据库设计

用户表

分类表

产品表

购物车表

支付信息表

订单明细表

收货地址表

项目和Git初始化

项目初始化

  • 使用idea创建一个maven项目,基于模板webapp
  • 使用Git命令创建README.md说明文件
  • 使用Git命令创建.gitignore文件,用来忽略上传的文件类型
# 所有的编译完的class文件不提交
*.class

#package file:所有的jar包,war包,ear等不提交
*.jar
*.war
*.ear

#kdiff3 ignore
*.orig

#maven ignore
target/

#eclipse ignore
.settings/
.project
.classpatch

#idea ignore
.idea/
/idea/
*.ipr
*.iml
*.iws

# temp file
*.log
*.cache
*.diff
*.patch
*.tmp

# system ignore
.DS_Store
Thumbs.db

Git初始化

  • 登录Git官网,登录账号,创建一个项目仓库
  • git init 初始化Git项目
  • git status 查看当前状态
  • git add . 把项目添加到本地仓库
  • git commit -am “first init” 把项目提交到本地仓库
  • git remote add origin 项目地址 把本地仓库提交到远程仓库
  • git branch 查看本地当前分支
  • git pull 拉取远程仓库到本地
  • git push -u -f origin master 提交本地仓库到远程master分支,并强制覆盖
  • git branch -r 查看远程仓库当前分支
  • git checkout -b v1.0 origin/master 在本地基于master分支创建一个v1.0的新分支
  • git push origin HEAD -u 把本地的v1.0分支提交到远程仓库

配置文件初始化

  • 初始化web.xml
  • 初始化spring配置文件
  • 初始化日志logback配置文件
  • 初始化springmvc配置文件
  • 使用mybatis generator 生成pojo和dao

通用类common

  • 常量类:Const
  • 通用数据响应类:ServerResponse
  • 响应状态枚举类:ResponseCode

Controller

portal

  • 用户登录:校验用户是否存在,返回用户信息
  • 用户登出
  • 用户注册
  • 校验用户名或邮箱
  • 获取当前登录用户的信息
  • 忘记密码,获取修改密码的问题
  • 忘记密码验证问题正确性
  • 忘记密码时重置密码
  • 登录状态下重置密码
  • 修改用户信息
  • 获取用户详情信息

manage/category

  • 添加商品分类
  • 更新商品分类
  • 获取分类的平级商品信息
  • 查询当前节点的id和递归子节点的id

product

  • 产品搜索
  • 动态排序列表
  • 商品详情

manage/product

  • 商品列表
  • 商品搜索
  • 图片上传
  • 富文本图片上传
  • 商品详情
  • 商品上下架
  • 增加商品
  • 更新商品
  • ftp服务器对接
  • 流读取Properties配置文件
  • joda-time

购物车

  • 加入商品
  • 更新商品数
  • 查询商品数
  • 移除商品
  • 单选/取消
  • 全选/取消
  • 购物车列表

收货地址管理

  • 添加地址
  • 删除地址
  • 更新地址
  • 地址列表
  • 地址分页
  • 地址详情

支付模块

  • 支付宝对接
  • 支付回调
  • 查询支付状态

订单模块

  • 前:创建订单
  • 前:商品信息
  • 前:订单列表
  • 前:订单详情
  • 前:取消订单
  • 后:订单列表
  • 后:订单搜索
  • 后:订单详情
  • 后:订单发货

Tomcat集群

  • 为什么要集群?
    • 提高服务的性能,并发能力,以及高可用性
    • 提供项目架构的横向扩展能力
  • 集群会带来哪些问题?怎样解决?
    • session登录信息存储以及读取的问题
    • 服务器定时任务并发的问题
    • 通过Nginx负载均衡进行请求转发
  • 单机部署多应用:

  • 添加Tomcat环境变量

  • 修改第二个tomcat的Catalina.bat和startup.bat

    CATALINA_BASE->CATALINA_2_BASE

    CATALINA_HOME->CATALINA_2_HOME

  • 修改配置文件server.xml。

    修改server port端口号

    修改connector port端口号

  • 配置Nginx,选择合适的策略

  • 修改系统host,启动tomcat和Nginx,访问测试

  • 多机部署多应用:在每台服务器上装一个tomcat即可,保证服务器之间的连通性

单点登录

引入jedis

  • pom
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.6.0</version>
</dependency>

配置文件

# redis config start
 
redis.ip=127.0.0.1
redis.port=6379

# 最大连接数
redis.max.total = 20

# 最大空闲数
redis.max.idle = 10

# 最小空闲数
redis.min.idle = 2

# 从jedis连接池获取连接时,校验并返回可用的连接
redis.test.borrow = true

# 把连接放回jedis连接池时,校验并返回可用的连接
redis.test.return = false

# redis config end

连接池

/**
 * Redis连接池
 */
public class RedisPool {
    private static JedisPool pool;//jedis连接池
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total","20"));//最大连接数
    private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle","10"));//在jedispool中最大的idle状态(空闲)的jedis实例的个数
    private static Integer minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle","2"));//在jedispool中最小的idle状态(空闲)的jedis实例的个数
    private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow","true"));//在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值true,则得到的jedis实例肯定是可以用的
    private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return", "true"));//在return一个jedis实例的时候,是否要进行验证操作,如果赋值true,则放回jedispool的jedis实例肯定是可以用的
    private static String redisIp = PropertiesUtil.getProperty("redis.ip", "127.0.0.1");
    private static Integer redisPort = Integer.parseInt(PropertiesUtil.getProperty("redis.port", "6379"));

    private static void initPool() {
        JedisPoolConfig config = new JedisPoolConfig();

        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnReturn(testOnReturn);
        config.setBlockWhenExhausted(true); //连接耗尽时,是否阻塞,false会抛出异常,true阻塞直到超时,
        pool = new JedisPool(config, redisIp, redisPort, 1000 * 2);
    }

    static {
        initPool();
    }
 
     public static Jedis getJedis(){
       return pool.getResource();
     }


    public static void returnBrokenResource(Jedis jedis) {
        pool.returnBrokenResource(jedis);
    }

    public static void returnResource(Jedis jedis) {
        pool.returnResource(jedis);
    }
}

连接池工具类

/**
 * 数据库连接池工具类
 */
@Slf4j
public class RedisPoolUtil {

    /**
     * 设置key的有效期,单位是秒
     * @param key
     * @param exTime
     * @return
     */
    public static Long expire(String key,int exTime){
        Jedis jedis = null;
        Long result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.expire(key,exTime);
        } catch (Exception e) {
            log.error("expire key:{} error",key,e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }
    
    public static String set(String key, String value) {
        Jedis jedis = null;
        String result = null;

        try {
            jedis = RedisPool.getJedis();
            result = jedis.set(key, value);
        } catch (Exception e) {
            log.error("set key:{} value:{} error", key, value, e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }

    /**
     * 设置键值对
     *
     * @param key
     * @param value
     * @param exTime 超时时间,单位是秒
     * @return
     */
    public static String setEx(String key, String value, int exTime) {
        Jedis jedis = null;
        String result = null;

        try {
            jedis = RedisPool.getJedis();
            result = jedis.setex(key, exTime, value);
        } catch (Exception e) {
            log.error("setex key:{} value:{} error", key, value, e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }

    /**重新设置键的超时时间
     * @param key
     * @param exTime 单位是秒
     * @return
     */
    public static Long setEx(String key,int exTime) {
        Jedis jedis = null;
        Long result = null;

        try {
            jedis = RedisPool.getJedis();
            result = jedis.expire(key, exTime);
        } catch (Exception e) {
            log.error("setex key:{} error", key, e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }

    public static String get(String key) {
        Jedis jedis = null;
        String result = null;

        try {
            jedis = RedisPool.getJedis();
            result = jedis.get(key);
        } catch (Exception e) {
            log.error("get key:{} error", key, e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }

    public static Long del(String key) {
        Jedis jedis = null;
        Long result = null;

        try {
            jedis = RedisPool.getJedis();
            result = jedis.del(key);
        } catch (Exception e) {
            log.error("del key:{} error", key, e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }
}

json序列化工具类

/**
 * json序列化工具
 */
@Slf4j
public class JsonUtil {
    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
        //对象的所有字段全部列入
        objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.ALWAYS);
        //取消默认转换timestamps形式
        objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
        //忽略空Bean转json的错误
        objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, false);
        //所有的日期格式都统一为以下样式,即yyyy-MM-dd HH:mm:ss
        objectMapper.setDateFormat(new SimpleDateFormat(DateTimeUtil.STANDARD_FORMAT));

        //忽略在json字符串中存在,但是在java对象中不存在对应属性的情况,防止错误
        objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /**
     * 对象转字符串
     *
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> String obj2String(T obj) {
        if (obj == null) {
            return null;
        }
        try {
            return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("parse object to string error", e);
            return null;
        }
    }

    /**
     * 对象转字符串并进行格式化
     *
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> String obj2StringPretty(T obj) {
        if (obj == null) {
            return null;
        }
        try {
            return obj instanceof String ? (String) obj : objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("parse object to string error", e);
            return null;
        }
    }

    /**
     * 字符串转对象
     *
     * @param str
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T string2Obj(String str, Class<T> clazz) {
        if (StringUtils.isEmpty(str) || clazz == null) {
            return null;
        }

        try {
            return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
        } catch (Exception e) {
            log.warn("parse string to object error", e);
            return null;
        }
    }

    /**
     * 字符串转嵌套集合
     *
     * @param str
     * @param typeReference
     * @param <T>
     * @return
     */
    public static <T> T string2Obj(String str, TypeReference<T> typeReference) {
        if (StringUtils.isEmpty(str) || typeReference == null) {
            return null;
        }

        try {
            return (T) (typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));
        } catch (Exception e) {
            log.warn("parse string to object error", e);
            return null;
        }
    }

    /**
     * 字符串转集合 如string转List<User>
     *
     * @param str
     * @param collectionClass 最外层泛型
     * @param elementClasses  内层泛型列表
     * @param <T>
     * @return
     */
    public static <T> T string2Obj(String str, Class<?> collectionClass, Class<?>... elementClasses) {
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
        try {
            return objectMapper.readValue(str, javaType);
        } catch (Exception e) {
            log.warn("Parse String to Object error", e);
            return null;
        }
    }

}

cookie工具类

/**
 * cookie工具类
 */
@Slf4j
public class CookieUtil {
    //X:domain=".happymmall.com"
    //a:A.happymmall.com            cookie:domain=A.happymmall.com;path="/"
    //b:B.happymmall.com            cookie:domain=B.happymmall.com;path="/"
    //c:A.happymmall.com/test/cc    cookie:domain=A.happymmall.com;path="/test/cc"
    //d:A.happymmall.com/test/dd    cookie:domain=A.happymmall.com;path="/test/dd"
    //e:A.happymmall.com/test       cookie:domain=A.happymmall.com;path="/test"
   //cookie的范围,只要是以.happymmall.com结尾的都可以共享同一个cookie
    private final static String COOKIE_DOMAIN = ".happymmall.com";
    private final static String COOKIE_NAME = "mmall_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);

        //单位是秒
        //如果这个maxage不设置的话,cookie就不会写入硬盘,而是写在内存,只在当前页面有效。
        ck.setMaxAge(60 * 60 * 24 * 365); //如果是-1,代表永久
        log.info("write cookieName:{},cookieValue:{}", ck.getName(), ck.getValue());
        response.addCookie(ck);
    }

    public static String readLoginToken(HttpServletRequest request) {
        Cookie[] cks = request.getCookies();
        if (cks != null) {
            for (Cookie ck : cks) {
                log.info("read cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
                    log.info("return cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                    return ck.getValue();
                }
            }
        }
        return null;
    }

    public static void delLoginToken(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cks = request.getCookies();
        if (cks != null) {
            for (Cookie ck : cks) {
                if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
                    ck.setDomain(COOKIE_DOMAIN);
                    ck.setPath("/");
                    ck.setMaxAge(0); //设置为0代表删除此cookie
                    log.info("del cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                    response.addCookie(ck);
                    return;
                }
            }
        }
    }
}

修改登录逻辑

/**
 * 用户登录:校验用户是否存在,返回用户信息
 * @param username
 * @param password
 * @param session
 * @return
 */
@RequestMapping(value = "login.do", method = RequestMethod.POST)
@ResponseBody
public ServerResponse login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse, HttpServletRequest httpServletRequest) {
    ServerResponse<User> response = iUserService.login(username,password);
    //登录成功后把用户信息放到session中
    if(response.isSuccess()) {
        // session中key用常量类中的常量,value为用户的数据
        //老版本方法session.setAttribute(Const.CURRENT_USER,response.getData());

        CookieUtil.writeLoginToken(httpServletResponse, session.getId());
        RedisPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
    }
    return response;
}
 
/**
 * 用户登出
 * @param
 * @return
 */
@RequestMapping(value = "logout.do", method = RequestMethod.POST)
@ResponseBody
public ServerResponse logout(HttpServletRequest request,HttpServletResponse response) {
    //移除session中的用户信息
    //session.removeAttribute(Const.CURRENT_USER);
    //token
    String loginToken = CookieUtil.readLoginToken(request);
    CookieUtil.delLoginToken(request, response);
    RedisPoolUtil.del(loginToken);
    return ServerResponse.createBySuccess();
}
 
/**
 * 获取当前登录用户的信息
 * @param
 * @return
 */
@RequestMapping(value = "get_user_info.do", method = RequestMethod.POST)
@ResponseBody
public ServerResponse<User> getUserInfo(HttpServletRequest request,HttpServletResponse response) {
    //判断用户是否登录
    //User user = (User) session.getAttribute(Const.CURRENT_USER);
    //token方式
    String loginToken = CookieUtil.readLoginToken(request);
    if (StringUtils.isEmpty(loginToken)) {
        return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
    }
    String userJsonStr = RedisPoolUtil.get(loginToken);
    User user = JsonUtil.string2Obj(userJsonStr, User.class);
    if (user != null) {
        return ServerResponse.createBySuccess(user);
    }
    return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
}
 
========================================================================================================
/**
 * 常量类
 * @auther QinFen
 *
 */
public class Const {
  /**
  * redis缓存过期时间
    */
  public interface RedisCacheExtime{
    int REDIS_SESSION_EXTIME = 60*30;//30分钟
    }
}

重置session有效期

  • web.xml
<!--二期重置session时间的filter-->
<filter>
    <filter-name>sessionExpireFilter</filter-name>
    <filter-class>com.mmall.controller.common.SessionExpireFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>sessionExpireFilter</filter-name>
    <url-pattern>*.do</url-pattern>
</filter-mapping>
  • 过滤器
/**
 * 单点登录重置session有效期
 */
public class SessionExpireFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        if (StringUtils.isEmpty(loginToken)) {
            //判断loginToken是否为空或“”
            //如果不为空,符合条件,继续拿user信息
            String userJsonStr = RedisPoolUtil.get(loginToken);
            User user = JsonUtil.string2Obj(userJsonStr, User.class);
            if (user != null) {
                //如果user不为空,则重置session时间,即调用redis的expire命令
                RedisPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {

    }
}

找回密码缓存迁移到redis

/**验证密保问题
 * @param username
 * @param question
 * @param answer
 * @return
 */
@Override
public ServerResponse<String> forgetCheckAnswer(String username, String question, String answer) {
    //通过username,问题,答案验证
    int resultCount = userMapper.checkAnswer(username,question,answer);
    if (resultCount > 0) {
        //说明问题和问题的答案是这个用户的,并且是正确的
        String forgetToken = UUID.randomUUID().toString();
        RedisPoolUtil.setEx(Const.TOKEN_PREFIX+username,forgetToken,606012);
        //返回一个token用于修改密码
        return ServerResponse.createBySuccess(forgetToken);
    }
    return ServerResponse.createByErrorMessage("问题的答案错误");
}

/**忘记密码时重置密码
 * @param username
 * @param passwordNew
 * @param forgetToken
 * @return
 */
@Override
public ServerResponse<String> forgetRestPassword(String username, String passwordNew, String forgetToken) {
    //判断参数中是否有token
    if (StringUtils.isBlank(forgetToken)) {
        return ServerResponse.createByErrorMessage("参数错误,token需要传递");
    }
    //查询用户名是否已经存在
    ServerResponse validResponse = this.checkValid(username,Const.USERNAME);
    if (validResponse.isSuccess()) {
        return ServerResponse.createByErrorMessage("用户不存在");
    }
    //获取服务端存储的token,用于和传入的参数比较
    String token = RedisPoolUtil.get(Const.TOKEN_PREFIX+username);
    if (StringUtils.isBlank(token)) {
        return ServerResponse.createByErrorMessage("token无效或者过期");
    }
    //如果token相同,允许修改密码
    if (StringUtils.equals(forgetToken,token)) {
        String md5Password = MD5Util.MD5EncodeUtf8(passwordNew);
        //修改密码
        int rowCount = userMapper.updatePasswordByUsername(username, md5Password);
        if (rowCount > 0) {
            return ServerResponse.createBySuccessMessage("修改密码成功");
        }
    }else {
        return ServerResponse.createByErrorMessage("token错误,请重新获取重置密码的token");
    }
    return ServerResponse.createByErrorMessage("修改密码失败");
}
  • 配置集群环境进行测试

Redis分布式

  • 启动两个redis,6379和6380
  • redis分片池
/**
 * redis 分片池
 */
public class RedisShardedPool {
    private static ShardedJedisPool pool;//jedis连接池
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total","20"));//最大连接数
    private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle","10"));//在jedispool中最大的idle状态(空闲)的jedis实例的个数
    private static Integer minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle","2"));//在jedispool中最小的idle状态(空闲)的jedis实例的个数
    private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow","true"));//在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值true,则得到的jedis实例肯定是可以用的
    private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return", "true"));//在return一个jedis实例的时候,是否要进行验证操作,如果赋值true,则放回jedispool的jedis实例肯定是可以用的
    private static String redis1Ip = PropertiesUtil.getProperty("redis1.ip", "127.0.0.1");
    private static Integer redis1Port = Integer.parseInt(PropertiesUtil.getProperty("redis1.port", "6379"));
    private static String redis2Ip = PropertiesUtil.getProperty("redis2.ip", "127.0.0.1");
    private static Integer redis2Port = Integer.parseInt(PropertiesUtil.getProperty("redis2.port", "6379"));

    private static void initPool() {
        JedisPoolConfig config = new JedisPoolConfig();

        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnReturn(testOnReturn);
        config.setBlockWhenExhausted(true); //连接耗尽时,是否阻塞,false会抛出异常,true阻塞直到超时,

        JedisShardInfo info1 = new JedisShardInfo(redis1Ip, redis1Port);
        JedisShardInfo info2 = new JedisShardInfo(redis2Ip, redis2Port);

        ArrayList<JedisShardInfo> jedisShardInfos = new ArrayList<>(2);
        jedisShardInfos.add(info1);
        jedisShardInfos.add(info2);

        pool = new ShardedJedisPool(config, jedisShardInfos, Hashing.MURMUR_HASH, Sharded.DEFAULT_KEY_TAG_PATTERN);
    }

    static {
        initPool();
    }

    public static ShardedJedis getJedis(){
        return pool.getResource();
    }

    public static void returnBrokenResource(ShardedJedis jedis) {
        pool.returnBrokenResource(jedis);
    }

    public static void returnResource(ShardedJedis jedis) {
        pool.returnResource(jedis);
    }
}
  • 工具类
/**
 * redis分片工具类
 */
@Slf4j
public class RedisShardedPoolUtil {

    /**
     * 设置key的有效期,单位是秒
     * @param key
     * @param exTime
     * @return
     */
    public static Long expire(String key,int exTime){
        ShardedJedis jedis = null;
        Long result = null;
        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.expire(key,exTime);
        } catch (Exception e) {
            log.error("expire key:{} error",key,e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    public static String set(String key, String value) {
        ShardedJedis jedis = null;
        String result = null;

        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.set(key, value);
        } catch (Exception e) {
            log.error("set key:{} value:{} error", key, value, e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    /**
     * 设置键值对
     *
     * @param key
     * @param value
     * @param exTime 超时时间,单位是秒
     * @return
     */
    public static String setEx(String key, String value, int exTime) {
        ShardedJedis jedis = null;
        String result = null;

        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.setex(key, exTime, value);
        } catch (Exception e) {
            log.error("setex key:{} value:{} error", key, value, e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    /**重新设置键的超时时间
     * @param key
     * @param exTime 单位是秒
     * @return
     */
    public static Long setEx(String key,int exTime) {
        ShardedJedis jedis = null;
        Long result = null;

        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.expire(key, exTime);
        } catch (Exception e) {
            log.error("setex key:{} error", key, e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    public static String get(String key) {
        ShardedJedis jedis = null;
        String result = null;

        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.get(key);
        } catch (Exception e) {
            log.error("get key:{} error", key, e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

    public static Long del(String key) {
        ShardedJedis jedis = null;
        Long result = null;

        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.del(key);
        } catch (Exception e) {
            log.error("del key:{} error", key, e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }
}

spring session

  • 特点:特点:侵入性低,但不支持分片策略
  • web.xml
<!--二期spring session方式实现单点登录-->
<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>*.do</url-pattern>
</filter-mapping>
 

  • spring配置文件
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
        <property name="maxInactiveIntervalInSeconds" value="1800" />
    </bean>
 
    <bean id="defaultCookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer">
        <property name="domainName" value=".happymmall.com" />
        <property name="useHttpOnlyCookie" value="true" />
        <property name="cookiePath" value="/" />
        <property name="cookieMaxAge" value="31536000" />
    </bean>
 
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="20"/>
    </bean>
 
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="127.0.0.1" />
        <property name="port" value="6379" />
        <property name="poolConfig" ref="jedisPoolConfig" />
    </bean>

  • userController
/**前端用户管理
 * @author qinfen
 */
@Controller
@RequestMapping("/user/springsession")
public class UserSpringSessionController {
    @Autowired
    private IUserService iUserService;

    /**
     * 用户登录:校验用户是否存在,返回用户信息
     * @param username
     * @param password
     * @param session
     * @return
     */
    @RequestMapping(value = "login.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse, HttpServletRequest httpServletRequest) {
        ServerResponse<User> response = iUserService.login(username,password);
        //登录成功后把用户信息放到session中
        if(response.isSuccess()) {
            // session中key用常量类中的常量,value为用户的数据
            session.setAttribute(Const.CURRENT_USER,response.getData());
        }
        return response;
    }

    /**
     * 用户登出
     * @param
     * @return
     */
    @RequestMapping(value = "logout.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse logout(HttpSession session,HttpServletRequest request,HttpServletResponse response) {
        //移除session中的用户信息
        session.removeAttribute(Const.CURRENT_USER);
        return ServerResponse.createBySuccess();
    }


    /**
     * 获取当前登录用户的信息
     * @param
     * @return
     */
    @RequestMapping(value = "get_user_info.do", method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> getUserInfo(HttpSession session,HttpServletRequest request,HttpServletResponse response) {
        //判断用户是否登录
        User user = (User) session.getAttribute(Const.CURRENT_USER);
        if (user != null) {
            return ServerResponse.createBySuccess(user);
        }
        return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
    }

}

Redis分布式锁

整合spring schedule定时任务

  • 一句话:作业调度,定时任务
  • applicationContext.xml
<!--二期新增spring schedule定时任务-->
<context:property-placeholder location="classpath:datasource.properties"/>
<task:annotation-driven />
  • 定时任务
/**
 * 定时关闭订单定时任务
 */
@Component
@Slf4j
public class CloseOrderTask {
    @Autowired
    private IOrderService iOrderService;

    @Scheduled(cron = "0 */1 * * * ?")//每分钟执行一次
    public void closeOrderTaskV1() {
        log.info("关闭订单定时任务启动");
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
        iOrderService.closeOrder(hour);
        log.info("关闭订单定时任务结束");

    }
}
 

  • 问题:在集群环境下我们只需要一台服务器执行任务就可以了,如果全部服务器都执行会发生数据错乱

实现分布式锁

image.png

/**
 * 定时关闭订单定时任务
 */
@Component
@Slf4j
public class CloseOrderTask {
    @Autowired
    private IOrderService iOrderService;
    @Autowired
    private RedissonManager redissonManager;

    @PreDestroy
    public void delLock(){
        RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);

    }
    //v1版本,不适用于分布式
    @Scheduled(cron = "0 */1 * * * ?")//每分钟执行一次
    public void closeOrderTaskV1() {
        log.info("关闭订单定时任务启动");
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
        iOrderService.closeOrder(hour);
        log.info("关闭订单定时任务结束");

    }

    //v2版本,分布式防止死锁,有缺陷
    //    @Scheduled(cron="0 */1 * * * ?")
    public void closeOrderTaskV2(){
        log.info("关闭订单定时任务启动");
        long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout","5000"));

        Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
        if(setnxResult != null && setnxResult.intValue() == 1){
            //如果返回值是1,代表设置成功,获取锁
            closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }else{
            log.info("没有获得分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }
        log.info("关闭订单定时任务结束");
    }

    //v3版本,分布式锁双重防死锁
    @Scheduled(cron="0 */1 * * * ?")
    public void closeOrderTaskV3(){
        log.info("关闭订单定时任务启动");
        long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout","5000"));
        Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
        if(setnxResult != null && setnxResult.intValue() == 1){
            closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }else{
            //未获取到锁,继续判断,判断时间戳,看是否可以重置并获取到锁
            String lockValueStr = RedisShardedPoolUtil.get(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            if(lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)){
                String getSetResult = RedisShardedPoolUtil.getSet(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
                //再次用当前时间戳getset。
                //返回给定的key的旧值,->旧值判断,是否可以获取锁
                //当key没有旧值时,即key不存在时,返回nil ->获取锁
                //这里我们set了一个新的value值,获取旧的值。
                if(getSetResult == null || (getSetResult != null && StringUtils.equals(lockValueStr,getSetResult))){
                    //真正获取到锁
                    closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                }else{
                    log.info("没有获取到分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                }
            }else{
                log.info("没有获取到分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            }
        }
        log.info("关闭订单定时任务结束");
    }

 
    private void closeOrder(String lockName){
        RedisShardedPoolUtil.expire(lockName,5);//有效期50秒,防止死锁
        log.info("获取{},ThreadName:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
        iOrderService.closeOrder(hour);
        RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        log.info("释放{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
        log.info("===============================");
    }
}

Redission框架

  • Redission是架设在Reids基础上的一个Java驻内存数据网格(In-Memory Data Grid)
  • Redission在基于NIO的Netty框架上,充分的利用了Reids键值数据库提供的一系列优势
  • 在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类
  • 使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度
  • 同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序之间的相互协作

  • 依赖
<!--Redission分布式锁框架-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>2.9.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-avro</artifactId>
    <version>2.9.0</version>
</dependency>
  • 管理器
/**
 * Redisson管理器
 */
@Component
@Slf4j
public class RedissonManager {

    private Config config = new Config();

    private Redisson redisson = null;

    public Redisson getRedisson() {
        return redisson;
    }

    private static String redis1Ip = PropertiesUtil.getProperty("redis1.ip");
    private static Integer redis1Port = Integer.parseInt(PropertiesUtil.getProperty("redis1.port"));
    private static String redis2Ip = PropertiesUtil.getProperty("redis2.ip");
    private static Integer redis2Port = Integer.parseInt(PropertiesUtil.getProperty("redis2.port"));

    @PostConstruct
    private void init(){
        try {
            config.useSingleServer().setAddress(new StringBuilder().append(redis1Ip).append(":").append(redis1Port).toString());

            redisson = (Redisson) Redisson.create(config);

            log.info("初始化Redisson结束");
        } catch (Exception e) {
            log.error("redisson init error",e);
        }
    }
}
  • 分布式锁
/**
 * 定时关闭订单定时任务
 */
@Component
@Slf4j
public class CloseOrderTask {
    @Autowired
    private IOrderService iOrderService;
    @Autowired
    private RedissonManager redissonManager;

    //Redisson方式
      @Scheduled(cron="0 */1 * * * ?")
    public void closeOrderTaskV4(){
        RLock lock = redissonManager.getRedisson().getLock(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        boolean getLock = false;
        try {
            if(getLock = lock.tryLock(0,50, TimeUnit.SECONDS)){
                log.info("Redisson获取到分布式锁:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
                int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
//                iOrderService.closeOrder(hour);
            }else{
                log.info("Redisson没有获取到分布式锁:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            log.error("Redisson分布式锁获取异常",e);
        } finally {
            if(!getLock){
                return;
            }
            lock.unlock();
            log.info("Redisson分布式锁释放锁");
        }
    }




    private void closeOrder(String lockName){
        RedisShardedPoolUtil.expire(lockName,5);//有效期50秒,防止死锁
        log.info("获取{},ThreadName:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
        iOrderService.closeOrder(hour);
        RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        log.info("释放{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
        log.info("===============================");
    }
}

下一篇 算法排序

Comments

Content