软件架构与思考

👉 所有文章
高性能 高性能设计模式
数据库 数据库分库分表指南 MySQL 建表参考 MySQL 初始化数据的一些方案 数据版本号
分布式 ID 分布式 ID 生成方案探讨
缓存 缓存 使用数据版本号保证缓存是最新数据 基于redis的二级缓存
微服务 如何实现远程调用 RPC 协议中的数据签名验签和加解密方案探讨 关于服务间调用循环依赖的一些思考 我所理解的负载均衡 一致性哈希 基于Redis的分布式会话管理系统 如何部署服务 灰度发布 如何区分上游和下游 日志级别
算法与协议 一个可扩展的 MQ 消息设计 Dynamo涉及的算法和协议 写时复制
任务分发 Gearman入门 如何使用redis构建异步任务处理程序
安全 关于对账的一些理解 一个简单可靠的 Dubbo 请求/响应数据签名方案
其他 使用卫语句减少 if else 嵌套

基于Redis的分布式会话管理系统


2013-09-01

在网站开发中,所谓session,也是一个特殊的cookie,常用于记录用户的登录状态。例如,当用户A正常登录启用的Sessin的网站B时候,网站B并不会将用户名、密码等显式的作为cookie,而是根据用户名、登录时间、登录IP等信息生成一个唯一的ID作为cookie,而在网站的服务器端则会记录该ID对应的用户名、失效时间等信息。这样,服务器要识别用户就是靠这个ID了。

以PHP为例,其使用session时候相应session信息是保存在文件中的,当然也可以设置以保存在数据库中。由于最近对redis有了些许的了解,同时考虑使用Java练练手,所以萌生了使用Redis做分布式会话管理系统的想法。

开发环境


Linux Mint 15,运行多个Redis实例;Windows 7,运行PostgreSQL 9.2,以及管理系统。

Redis配置


最好的方式是这些服务器全在内网中运行,由于懒得去找路由器,故做如下布局:4个Redis实例所在的linux mint使用ip 202.201.13.186,分别使用这些端口6301、6302、6303、6304。

首先在mint中安装redis-server:

sudo apt-get install redis-server

这同时会把redis-cli等工具安装了。 在某处新建一个目录redis,在redis目录下依次建立以下文件:

redis-01.conf redis-01.pid redis-01.log
redis-02.conf redis-02.pid redis-02.log
redis-03.conf redis-03.pid redis-03.log
redis-04.conf redis-04.pid redis-04.log

redis-01.conf内容如下:

daemonize yes
pidfile ./redis-01.pid
port 6301
timeout 300
loglevel debug
logfile ./redis-01.log
requirepass 111

redis-02.conf内容如下:

daemonize yes
pidfile ./redis-02.pid
port 6302
timeout 300
loglevel debug
logfile ./redis-02.log
requirepass 222

redis-03.conf内容如下:

daemonize yes
pidfile ./redis-03.pid
port 6303
timeout 300
loglevel debug
logfile ./redis-03.log
requirepass 333

redis-04.conf内容如下:

daemonize yes
pidfile ./redis-04.pid
port 6304
timeout 300
loglevel debug
logfile ./redis-04.log
requirepass 444

关于这些配置文件,以redis-04.conf为例,daemonize指定redis是否以DAEMON形式运行,pidfile指定保存redis实例的pid的文件,port指定端口,timeout指定超时时间(以秒为单位),loglevel指定日志记录等级,logfile指定日志输入位置,requirepass指定连接redis需要的密码。

配置文件完成后,依次启动四个redis server实例:

user@myhost ~/Desktop/redis $ redis-server redis-01.conf
user@myhost ~/Desktop/redis $ redis-server redis-02.conf
user@myhost ~/Desktop/redis $ redis-server redis-03.conf
user@myhost ~/Desktop/redis $ redis-server redis-04.conf

随机测试一个启动的server:

user@myhost ~/Desktop/redis $ redis-cli -h 127.0.0.1 -p 6301 -a 111
redis 127.0.0.1:6301> set mykey myvalue
OK
redis 127.0.0.1:6301> get mykey
"myvalue"
redis 127.0.0.1:6301> del mykey
(integer) 1
redis 127.0.0.1:6301> get mykey
(nil)
redis 127.0.0.1:6301> exit

使用PostgreSQL保存用户信息


我们使用PostgreSQL保存用户的信息,下面是一些必要的数据库、表创建语句:

CREATE DATABASE test_db ENCODING='UTF8';
CREATE SEQUENCE increment_num INCREMENT 1 START 1;
CREATE TABLE session (
user_id INT DEFAULT NEXTVAL('increment_num'),
user_name VARCHAR(20),
user_email VARCHAR(40),
user_passwd VARCHAR(50),
CONSTRAINT primary_key PRIMARY KEY (user_id),
CONSTRAINT unique_email UNIQUE (user_email),
CONSTRAINT unique_name UNIQUE (user_name)
);
CREATE USER test_user PASSWORD '123456';
GRANT ALL ON session  TO test_user;
GRANT ALL ON increment_num  TO test_user;

在数据库test_db中创建表session,而用户test_user具有对session表的所有权限。

插入三条数据:

INSERT INTO session (user_name, user_email, user_passwd) VALUES ('hei','hei@163.com',md5('111'));
INSERT INTO session (user_name, user_email, user_passwd) VALUES ('xiaoming','xiaoming@163.com',md5('111'));
INSERT INTO session (user_name, user_email, user_passwd) VALUES ('obama','obama@163.com',md5('111'));

注意不可写成下面的形式:

INSERT INTO session (user_name, user_email, user_passwd) VALUES ("hei","hei@163.com",md5("111"));

这会产生找不到某某字段的错误。

使用redis分布式存储session信息


用户登陆时候需要输入用户名,密码和登录IP。当然在实际生活中我们并不需要输入IP,你的机子已经帮你做了,这里要输入IP是为了简化代码。

假定所有用户名都是以字母开头,设该字母为firstLetter。

如果firstLetter介于a和g以及A和G之间,则该用户登陆时候将其session信息保存在port为6301的redis server中;
如果firstLetter介于h和n以及H和N之间,则该用户登陆时候将其session信息保存在port为6302的redis server中;
如果firstLetter介于o和t以及O和T之间,则该用户登陆时候将其session信息保存在port为6303的redis server中;
如果firstLetter介于u和z以及U和Z之间,则该用户登陆时候将其session信息保存在port为6304的redis server中。

每个session为一条记录,出于简化目的,key是IP和用户名和登录时间连在一起,例如可能是"192.168.1.222obama20130505120343",当然在实际项目中可不能这样;value是一个map,map中三个key分别是user_iduser_nameuser_email

Java如何操作redis中的数据


采用的是jedis包,下载见点这里

java操作redis

APP测试


源文件布局如下:

运行User类:

help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>4
请输入您的name>>>obama
请输入您的session ID>>>            } else if (4 == choose) {
                userLogout();姓名:obama
成功退出
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>1
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>1
开始登录吧
请输入用户名>>>obama
请输入密码>>>111
请输入登录IP>>>192.168.1.123
信息正确,进行登录。。。
登录时间:201383117367
用户名为:obama ,获取相应的server
姓名:obama
redis server信息:ip:202.201.13.186;port:6303
登录成功,请记住你的session id:192.168.1.123obama201383117367
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>4
请输入您的name>>>obama
请输入您的session ID>>>192.168.1.123obama201383117367
姓名:obama
成功退出
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>2
请输入您的name>>>obama
请输入您的session ID>>>192.168.1.123obama201383117367
姓名:obama
尚未登录 或者 会话过期
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>1
开始登录吧
请输入用户名>>>obama
请输入密码>>>111
请输入登录IP>>>192.168.1.222
信息正确,进行登录。。。
登录时间:2013831173751
用户名为:obama ,获取相应的server
姓名:obama
redis server信息:ip:202.201.13.186;port:6303
登录成功,请记住你的session id:192.168.1.222obama2013831173751
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>3
请输入您的name>>>obama
请输入您的session ID>>>192.168.1.222obama2013831173751
姓名:obama
您已经登录
姓名:obama
会话失效时间已经更新
help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出
>>

运行User期间在redis server中观测数据情况:

user@myhost ~/Desktop/redis $ redis-cli -h 127.0.0.1 -p 6303 -a 333
redis 127.0.0.1:6303> keys *
(empty list or set)
redis 127.0.0.1:6303> keys *
1) "192.168.1.222obama2013831173751"
redis 127.0.0.1:6303> ttl 192.168.1.222obama2013831173751
(integer) 1169
redis 127.0.0.1:6303> ttl 192.168.1.222obama2013831173751
(integer) 1168
redis 127.0.0.1:6303> ttl 192.168.1.222obama2013831173751
(integer) 1196
redis 127.0.0.1:6303> 

源码


User.java:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Calendar;
import java.util.Scanner;

public class User {
    /*
     * 模拟登陆
     */
    public static void userLogin() throws IOException {
        String userName = "";
        String userPasswd = "";
        String userIp = "";
        String loginTime = "";
        RedisAdmin myRedisAdmin = new RedisAdmin();
        // 输入信息
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.printf("请输入用户名>>>");
        userName = br.readLine();
        System.out.printf("请输入密码>>>");
        userPasswd = br.readLine();
        System.out.printf("请输入登录IP>>>");
        userIp = br.readLine();
        // br.close(); //若不注释,那么多次调用userLogin()函数时候就会有错误,why?
        // 原因见:http://zhidao.baidu.com/question/25112752.html
        // 判断用户是否存在
        if (PsqlAdmin.isUserExists(userName, userPasswd)) {
            System.out.println("信息正确,进行登录。。。");
        } else {
            System.out.println("对不起,用户名或密码错误!");
        }
        // 获取登录时间
        Calendar now = Calendar.getInstance();
        loginTime = "" + now.get(Calendar.YEAR) + (now.get(Calendar.MONTH) + 1)
                + now.get(Calendar.DAY_OF_MONTH)
                + now.get(Calendar.HOUR_OF_DAY) + now.get(Calendar.MINUTE)
                + now.get(Calendar.SECOND);
        System.out.println("登录时间:" + loginTime);
        // 基本信息已经获得,现在就把它放在redis中
        if (myRedisAdmin.addSession(userIp, userName, userPasswd, loginTime)) {
            System.out.println("登录成功,请记住你的session id:"
                    + myRedisAdmin.genSessionID(userIp, userName, loginTime));
        } else {
            System.out.println("登录失败");
        }
    }
    /*
     * 判断是否已经登录
     */
    public static void isLogin() throws IOException {
        String userName;
        String sessionID;
        RedisAdmin myRedisAdmin = new RedisAdmin();
        // 输入信息
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.printf("请输入您的name>>>");
        userName = br.readLine();
        System.out.printf("请输入您的session ID>>>");
        sessionID = br.readLine();
        if (myRedisAdmin.existsSession(userName, sessionID)) {
            System.out.println("您已经登录");
        } else {
            System.out.println("尚未登录 或者 会话过期");
        }
    }
    /*
     * 用户做些动作,更新session的expire time
     */
    public static void userAction() throws IOException {
        String userName = "";
        String sessionID = "";
        RedisAdmin myRedisAdmin = new RedisAdmin();
        // 输入信息
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.printf("请输入您的name>>>");
        userName = br.readLine();
        System.out.printf("请输入您的session ID>>>");
        sessionID = br.readLine();
        if (myRedisAdmin.existsSession(userName, sessionID)) {
            System.out.println("您已经登录");
            if (myRedisAdmin.updateSession(userName, sessionID)) {
                System.out.println("会话失效时间已经更新");
            } else {
                System.out.println("会话失效时间更新失败%>_<%");
            }
        } else {
            System.out.println("尚未登录 或者 会话过期");
        }
    }
    /*
     * 用户注销
     */
    public static void userLogout() throws IOException {
        String userName = "";
        String sessionID = "";
        RedisAdmin myRedisAdmin = new RedisAdmin();
        // 输入信息
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.printf("请输入您的name>>>");
        userName = br.readLine();
        System.out.printf("请输入您的session ID>>>");
        sessionID = br.readLine();
        if (myRedisAdmin.delSession(userName, sessionID)) {
            System.out.println("成功退出");
        } else {
            System.out.println("尚未登录 或者 会话过期");
        }
    }

    public static void main(String[] args) throws IOException {

        int choose = 0;
        Scanner input;
        while (true) {
            System.out.println("help >>1:登录,2:看看自己是否登录,3:登录后做一些操作,4:注销,5:退出");
            System.out.printf(">>");
            choose = 0;
            input = new Scanner(System.in);
            if (input.hasNextInt()) {
                choose = input.nextInt();
            }

            if (1 == choose) {
                System.out.println("开始登录吧");
                userLogin();
            } else if (2 == choose) {
                isLogin();
            } else if (3 == choose) {
                userAction();
            } else if (4 == choose) {
                userLogout();
            } else if (5 == choose) {
                break;
            } else { // do nothing
            } 
        }
        input.close();
    }
}

RedisAdmin.java:

import java.util.HashMap;
import java.util.Map;

import redis.clients.jedis.Jedis;

public class RedisAdmin {
    private int EXPIRE_TIME = 1200;
    private HashMap<String, String> redisServer01, redisServer02,
            redisServer03, redisServer04;

    /*
     * 初始化各个redis server的信息
     */
    public RedisAdmin() {
        redisServer01 = new HashMap<String, String>();
        redisServer01.put("ip", "202.201.13.186");
        redisServer01.put("port", "6301");
        redisServer01.put("pass", "111");
        redisServer02 = new HashMap<String, String>();
        redisServer02.put("ip", "202.201.13.186");
        redisServer02.put("port", "6302");
        redisServer02.put("pass", "222");
        redisServer03 = new HashMap<String, String>();
        redisServer03.put("ip", "202.201.13.186");
        redisServer03.put("port", "6303");
        redisServer03.put("pass", "333");
        redisServer04 = new HashMap<String, String>();
        redisServer04.put("ip", "202.201.13.186");
        redisServer04.put("port", "6304");
        redisServer04.put("pass", "444");
    }

    /*
     * 根据用户名判断该用户的session信息应该放在哪个redis server中
     */
    public HashMap<String, String> getServer(String name) {
        System.out.println("姓名:" + name);
        char firstLetter = name.charAt(0);
        if (('a' <= firstLetter && firstLetter <= 'g')
                || ('A' <= firstLetter && firstLetter <= 'G')) {
            return this.redisServer01;
        } else if (('h' <= firstLetter && firstLetter <= 'n')
                || ('H' <= firstLetter && firstLetter <= 'N')) {
            return this.redisServer02;
        } else if (('o' <= firstLetter && firstLetter <= 't')
                || ('O' <= firstLetter && firstLetter <= 'T')) {
            return this.redisServer03;
        } else {
            return this.redisServer04;
        }

    }

    /*
     * 生成sessionId
     */
    public String genSessionID(String ip, String userName, String loginTime) {
        String sessionID = ip + userName + loginTime;
        return sessionID;
    }

    /*
     * 判断sessionId是否存在
     */
    public boolean existsSession(String userName, String sessionID) {
        try {
            HashMap<String, String> server = this.getServer(userName);
            Jedis redis = new Jedis(server.get("ip"), Integer.parseInt(server
                    .get("port")));// 连接redis
            redis.auth(server.get("pass"));// 验证密码
            return redis.exists(sessionID);
        } catch (Exception ee) {
            ee.printStackTrace();
        }
        return false;
    }

    /*
     * 向redis集群中添加session
     */
    public boolean addSession(String ip, String userName, String passwd,
            String loginTime) {
        try {
            System.out.println("用户名为:" + userName + " ,获取相应的server");
            HashMap<String, String> server = this.getServer(userName);
            System.out.println("redis server信息:ip:" + server.get("ip")
                    + ";port:" + server.get("port"));
            HashMap<String, String> userInfo = PsqlAdmin.getUserInfo(userName,
                    passwd);
            Map<String, String> session = new HashMap<>();
            session.put("user_id", userInfo.get("user_id"));
            session.put("user_name", userInfo.get("user_name"));
            session.put("user_email", userInfo.get("user_email"));
            String sessionID = genSessionID(ip, userName, loginTime);
            Jedis redis = new Jedis(server.get("ip"), Integer.parseInt(server
                    .get("port")));// 连接redis
            redis.auth(server.get("pass"));// 验证密码
            redis.hmset(sessionID, session);
            redis.expire(sessionID, EXPIRE_TIME);
            return true;
        } catch (Exception ee) {
            ee.printStackTrace();
        }
        return false;
    }

    /*
     * 更新EXPIRE TIME(过期时间)
     */
    public boolean updateSession(String userName, String sessionID) {
        try {
            HashMap<String, String> server = this.getServer(userName);
            Jedis redis = new Jedis(server.get("ip"), Integer.parseInt(server
                    .get("port")));// 连接redis
            redis.auth(server.get("pass"));// 验证密码
            redis.expire(sessionID, EXPIRE_TIME);
            return true;
        } catch (Exception ee) {
            ee.printStackTrace();
        }
        return false;
    }

    /*
     * 删除session
     */
    public boolean delSession(String userName, String sessionID) {
        try {
            HashMap<String, String> server = this.getServer(userName);
            Jedis redis = new Jedis(server.get("ip"), Integer.parseInt(server
                    .get("port")));// 连接redis
            redis.auth(server.get("pass"));// 验证密码
            redis.expire(sessionID, 0); // 0 秒后过期
            return true;
        } catch (Exception ee) {
            ee.printStackTrace();
        }
        return false;
    }
}

PsqlAdmin.java:

/*
 * 代码结构不够好啊
 */
import java.sql.*;
import java.util.HashMap;

public class PsqlAdmin {
    public PsqlAdmin() {

    }
    /*
     * 向数据库中添加用户
     */
    public static boolean addUser(String userName, String userEmail,
            String userPasswd) {
        String sql = " INSERT INTO session (user_name, user_email, user_passwd) VALUES ('"
                + userName
                + "','"
                + userEmail
                + "',"
                + "md5('"
                + userPasswd
                + "'))"; // 别用这个引号“"”
        System.out.println(sql);
        try {
            Class.forName("org.postgresql.Driver").newInstance();
            String url = "jdbc:postgresql://localhost:5432/test_db";
            Connection con = DriverManager.getConnection(url, "test_user",
                    "123456");
            Statement st = con.createStatement();
            int count = st.executeUpdate(sql); // 这句有问题
            System.out.println(sql);
            System.out.println("插入记录的数目:" + count);
            st.close();
            con.close();
            return true;
        } catch (Exception ee) {
            ee.printStackTrace();
            return false;
        }
    }
    /*
     * 显示数据库中已有的用户的信息
     */
    public static boolean showUser() {
        try {
            Class.forName("org.postgresql.Driver").newInstance();
            String url = "jdbc:postgresql://localhost:5432/test_db";
            Connection con = DriverManager.getConnection(url, "test_user",
                    "123456");
            Statement st = con.createStatement();
            String sql = "SELECT user_name, user_email FROM session";
            ResultSet rs = st.executeQuery(sql);
            while (rs.next()) {
                System.out.println("姓名:" + rs.getString("user_name"));
                System.out.println("电邮:" + rs.getString("user_email"));
            }
            rs.close();
            st.close();
            con.close();
            return true;
        } catch (Exception ee) {
            return false;
        }
    }
    /*
     * 根据用户名和密码判断数据库中是否存在相应的记录
     */
    public static boolean isUserExists(String name, String passwd) {
        try {
            Class.forName("org.postgresql.Driver").newInstance();
            String url = "jdbc:postgresql://localhost:5432/test_db";
            Connection con = DriverManager.getConnection(url, "test_user",
                    "123456");
            Statement st = con.createStatement();
            String sql = "SELECT COUNT(*) AS num FROM session WHERE user_name = \'"
                    + name + "\' and user_passwd=md5(\'" + passwd + "\')";
            // System.out.println(sql);
            ResultSet rs = st.executeQuery(sql);
            int exist = 0;
            while (rs.next()) {
                exist = rs.getInt("num");
            }
            rs.close();
            st.close();
            con.close();
            if (0 == exist) {
                return false;
            } else {
                return true;
            }
        } catch (Exception ee) {
            ee.printStackTrace();
            return false;
        }
    }
    /*
     * 根据用户名和密码从数据库中获取详尽的用户信息
     */
    public static HashMap<String, String> getUserInfo(String name, String passwd) {
        HashMap<String, String> userInfo = new HashMap<>();
        try {
            Class.forName("org.postgresql.Driver").newInstance();
            String url = "jdbc:postgresql://localhost:5432/test_db";
            Connection con = DriverManager.getConnection(url, "test_user",
                    "123456");
            Statement st = con.createStatement();
            String sql = "SELECT *  FROM session WHERE user_name = \'"
                    + name + "\' and user_passwd=md5(\'" + passwd + "\')";
            // System.out.println(sql);
            ResultSet rs = st.executeQuery(sql);
            while (rs.next()) {
                userInfo.put("user_id", String.valueOf(rs.getInt("user_id")));
                userInfo.put("user_name", rs.getString("user_name"));
                userInfo.put("user_email", rs.getString("user_email"));
            }
            rs.close();
            st.close();
            con.close();
        } catch (Exception ee) {
            ee.printStackTrace();
        }
        return userInfo;
    }
    /*
     * public static void main(String[] args) { boolean state = addUser("obama",
     * "obama@163.com", "111"); if (state == false) {
     * System.out.println("插入失败"); } state = showUser(); if (state == false) {
     * System.out.println("查询失败"); } }
     */
}


( 本文完 )

文章目录