基于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("查询失败"); } }
     */
}


( 本文完 )