首頁 >資料庫 >Redis >redis中分散式session不一致性怎麼辦

redis中分散式session不一致性怎麼辦

青灯夜游
青灯夜游轉載
2021-11-12 10:47:142661瀏覽

分散式session不一致性怎麼辦?以下這篇文章為大家介紹一下redis中分散式session不一致性的解決方案,希望對大家有幫助!

redis中分散式session不一致性怎麼辦

分散式session不一致性解決方案

一、Session有什麼作用?

  • Session 是用戶端與伺服器通訊會話追蹤技術,伺服器與用戶端保持整個通訊的會話基本資訊。 【相關推薦:Redis影片教學

  • 客戶端在第一次存取服務端的時候,服務端會回應一個sessionId並且將它存入到本地在Cookie中,在之後的存取會將cookie中的sessionId放入到請求頭中去存取伺服器,

  • 如果透過這個sessionId沒有找到對應的資料,那麼伺服器會建立一個新的sessionId並且會回應給客戶端。

二、分散式Session有什麼問題?

單一伺服器web應用程式中,session資訊只需存在該伺服器中,這是我們前幾年最常接觸的方式

但是近年來隨著分散式系統的流行,單系統已經不能滿足日益增長的百萬級用戶的需求,集群方式部署伺服器已在很多公司運用起來

當高並發量的請求到達服務端的時候通過負載平衡的方式分發到叢集中的某個伺服器,這樣就有可能導致同一個使用者的多次請求被分發到叢集的不同伺服器上,就會出現取不到session資料的情況,於是session的共享就成了一個問題。

redis中分散式session不一致性怎麼辦

三、服務做叢集一般是怎麼樣做的?

  • SpringBoot項目,那麼只要改下連接埠號碼啟動幾個,然後用nginx統一做反向代理。
  • SpringCloud微服務項目,那麼這個時候,可以使用ribbon本地負載平衡。

四、nginx負載平衡與ribbon負載平衡的差異

  • nginx做負載平衡是伺服器端的負載平衡,統一存取一個位址,根據負載平衡演算法存取決定存取那一個伺服器。
  • ribbon負載平衡,這是本地負載平衡(客戶端負載平衡),把提供服務的客戶端位址都快取記錄下來,根據本地的演算法實現負載平衡。

五、Session一致性解決方案

#1. session複製(同步)

redis中分散式session不一致性怎麼辦

想法:多個服務端之間相互同步session,這樣每個服務端之間都包含全部的session

優點:服務端支援的功能,應用程式不需要修改代碼

缺點:

  • session的同步需要資料傳輸,佔內網頻寬,有時延
  • 所有服務端都包含所有session數據,資料量受記憶體限制,無法水平擴充

#2. 客戶端儲存法 

redis中分散式session不一致性怎麼辦

想法:服務端儲存所有使用者的session,記憶體佔用較大,可以將session儲存到瀏覽器cookie中,每個端只要儲存一個使用者的資料了

優點:服務端不需要儲存

缺點:

  • 每次http請求都攜帶session,佔外網頻寬
  • 資料儲存在端上,並在網路傳輸,存在洩漏、篡改、竊取等安全隱患
  • session儲存的資料大小和網域cookie個數都受限制的

註:該方案雖然不常用,但確實是一種思路。

3. 反向代理hash一致性

# 想法:服務端為了確保高可用,有多台冗餘,反向代理層能不能做一些事情,讓同一個使用者的請求保證落在一台服務端上呢?

方案一:四層代理hash

redis中分散式session不一致性怎麼辦

#反向代理層使用使用者的ip來做hash,以保證同一個ip的請求落在同一個服務端上

方案二:七層代理hash

redis中分散式session不一致性怎麼辦

#

反向代理程式使用http協定中的某些業務屬性來做hash,例如sid,city_id,user_id等,能夠更靈活的實作hash策略,以確保同一個瀏覽器使用者的請求落在同一個伺服器上

優點:

  • 只需要改nginx配置,不需要修改應用程式碼
  • 負載平衡,只要hash屬性是均勻的,多台服務端的負載是均衡的
  • 可以支援服務端水平擴展(session同步法是不行的,受記憶體限制)

缺點:

    ##如果服務端重啟,一部分session會遺失,產生業務影響,例如部分使用者重新登入
  • 如果服務端水平擴展,rehash後session重新分佈,也會有一部分使用者路由不到正確的session
session一般是有有效期限的,所有不足中的兩點,可以認為等同於部分session失效,一般問題不大。

對於四層hash還是七層hash,個人推薦前者:讓專業的軟體做專業的事情,反向代理就負責轉發,盡量不要引入應用層業務屬性,除非不得不這麼做(例如,有時候多機房多活需要依照業務屬性路由到不同機房的伺服器)。

四層、七層負載平衡的差異

#4.後端統一集中儲存

redis中分散式session不一致性怎麼辦

#優點:

    沒有安全隱患
  • #可以水平擴展,資料庫/快取水平切分即可
  • 服務端重啟或擴容都不會有session遺失
不足:增加了一次網路調用,並且需要修改應用程式碼

對於db儲存還是cache,個人推薦後者:session讀取的頻率會很高,資料庫壓力會比較大。如果有session高可用需求,cache可以做高可用,但大部分情況下session可以遺失,一般也不需要考慮高可用。

總結

保證session一致性的架構設計常見方法:

    session同步法:多台服務端相互同步資料
  • 客戶端儲存法一個使用者只儲存自己的資料
  • 反向代理hash一致性四層hash和七層hash都可以做,保證一個使用者的請求落在一台服務端上
  • 後端統一儲存服務端重新啟動與擴充,session也不會遺失(建議後端cache統一儲存)

六、案例實戰:SpringSession redis解決分散式session不一致性問題

#步驟1:加入SpringSession、redis的依賴套件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

步驟2:設定檔

# 为某个包目录下 设置日志
logging.level.com.ljw=debug

# 设置session的存储方式,采用redis存储
spring.session.store-type=redis
# session有效时长为10分钟
server.servlet.session.timeout=PT10M

## Redis 配置
## Redis数据库索引(默认为0)
spring.redis.database=0
## Redis服务器地址
spring.redis.host=127.0.0.1
## Redis服务器连接端口
spring.redis.port=6379
## Redis服务器连接密码(默认为空)
spring.redis.password=

步驟3: 設定攔截器

@Configuration
public class SessionConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SecurityInterceptor())
                //排除拦截的2个路径
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/logout")
                //拦截所有URL路径
                .addPathPatterns("/**");
    }
}
@Configuration
public class SecurityInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        HttpSession session = request.getSession();
        //验证当前session是否存在,存在返回true true代表能正常处理业务逻辑
        if (session.getAttribute(session.getId()) != null){
            log.info("session拦截器,session={},验证通过",session.getId());
            return true;
        }
        //session不存在,返回false,并提示请重新登录。
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write("请登录!!!!!");
        log.info("session拦截器,session={},验证失败",session.getId());
        return false;
    }
}

    HandlerInterceptor
    • preHandle:在業務處理器處理請求之前被呼叫。預處理,可以進行編碼、安全控制、權限校驗等處理;
    • postHandle:在業務處理器處理請求執行完成後,產生檢視之前執行。後處理(調用了Service並返回ModelAndView,但未進行頁面渲染),有機會修改ModelAndView
    • afterCompletion:在DispatcherServlet完全處理完請求後被調用,可用於清理資源等。返回處理(已經渲染了頁面)

步驟4:控制器

@RestController
@RequestMapping(value = "/user")
public class UserController {

    Map<String, User> userMap = new HashMap<>();

    public UserController() {
        //初始化2个用户,用于模拟登录
        User u1=new User(1,"user1","user1");
        userMap.put("user1",u1);
        User u2=new User(2,"user2","user2");
        userMap.put("user2",u2);
    }

    @GetMapping(value = "/login")
    public String login(String username, String password, HttpSession session) {
        //模拟数据库的查找
        User user = this.userMap.get(username);
        if (user != null) {
            if (!password.equals(user.getPassword())) {
                return "用户名或密码错误!!!";
            } else {
                session.setAttribute(session.getId(), user);
                log.info("登录成功{}",user);
            }
        } else {
            return "用户名或密码错误!!!";
        }
        return "登录成功!!!";
    }

    /**
     * 通过用户名查找用户
     */
    @GetMapping(value = "/find/{username}")
    public User find(@PathVariable String username) {
        User user=this.userMap.get(username);
        log.info("通过用户名={},查找出用户{}",username,user);
        return user;
    }

    /**
     *拿当前用户的session
     */
    @GetMapping(value = "/session")
    public String session(HttpSession session) {
        log.info("当前用户的session={}",session.getId());
        return session.getId();
    }

    /**
     * 退出登录
     */
    @GetMapping(value = "/logout")
    public String logout(HttpSession session) {
        log.info("退出登录session={}",session.getId());
        session.removeAttribute(session.getId());
        return "成功退出!!";
    }

}

步驟5:實體類別

@Data
public class User implements  Serializable{

    private int id;
    private String username;
    private String password;

    public User(int id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

}

步驟6:存取測試##先登入:

http://127.0.0.1:8080/user/login?username=user1&password=user1

再查詢

http://127.0.0.1:8080/user/find/user1

七、剖析SpringSession的redis原理

#步驟1:分析SpringSession的redis資料結構

127.0.0.1:6379> keys *
1) "spring:session:sessions:9889ccfd-f4c9-41e5-b9ab-a77649a7bb6a"
2) "spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b"
3) "spring:session:expirations:1635413520000"
4) "spring:session:sessions:expires:9889ccfd-f4c9-41e5-b9ab-a77649a7bb6a"
5) "spring:session:expirations:1635412980000"
6) "spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b"
共同點:3個key都是以spring:session:開頭的,代表了SpringSession的redis資料。

查詢類型

127.0.0.1:6379> type spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
hash
127.0.0.1:6379> hgetall spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
// session的创建时间
1) "creationTime"
2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01|\xc5\xdb\xecu"
// sesson的属性,存储了user对象
3) "sessionAttr:d3434f61-4d0a-4687-9070-610bd7790f3b"
4) "\xac\xed\x00\x05sr\x00\x1ecom.ljw.redis.controller.User\x16\"_m\x1b\xa0W\x7f\x02\x00\x03I\x00\x02idL\x00\bpasswordt\x00\x12Ljava/lang/String;L\x00\busernameq\x00~\x00\x01xp\x00\x00\x00\x01t\x00\x05user1q\x00~\x00\x03"
//最后的访问时间
5) "lastAccessedTime"
6) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01|\xc5\xe1\xc7\xed"
//失效时间 100分钟
7) "maxInactiveInterval"
8) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x17p"

步驟2:分析SpringSession的redis過期策略對於過期數據,一般有三種刪除策略:

    定時刪除,即在設定鍵的過期時間的同時,建立一個定時器, 當鍵的過期時間到來時,立即刪除。
  • 惰性刪除,也就是在存取鍵的時候,判斷鍵是否過期,過期則刪除,否則傳回該鍵值。
  • 定期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

  • redis删除过期数据采用的是懒性删除+定期删除组合策略,也就是数据过期了并不会及时被删除。

  • 但由于redis是单线程,并且redis对删除过期的key优先级很低;如果有大量的过期key,就会出现key已经过期但是未删除。

  • 为了实现 session 过期的及时性,spring session 采用了定时删除+惰性删除的策略。

定时删除

127.0.0.1:6379> type spring:session:expirations:1635413520000
set
127.0.0.1:6379> smembers  spring:session:expirations:1635413520000
1) "\xac\xed\x00\x05t\x00,expires:d3434f61-4d0a-4687-9070-610bd7790f3b"

redis中分散式session不一致性怎麼辦

2) "spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b" 
3) "spring:session:expirations:1635413520000" 
6) "spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b"
  • 1635412980000 是时间戳,等于 2021-10-28 17:23:00,即是该可以在这个时刻过期
  • springsession 定时(1分钟)轮询,删除spring:session:expirations:[?] 的过期成员元素,例如:spring:session:expirations:1635413520000
  • springsesion 定时检测超时的key的值,根据值删除seesion,例如key:spring:session:expirations:1635413520000,值为(sessionId):d3434f61-4d0a-4687-9070-610bd7790f3b的seesion

惰性删除

127.0.0.1:6379> type spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
string
127.0.0.1:6379> get spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
""
127.0.0.1:6379> ttl spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
(integer) 3143
127.0.0.1:6379>
  • 访问 spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b的时候,判断key是否过期,过期则删除,否则返回改进的值。
  • 例如 访问spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b的时候,判断 ttl 是否过期,过期就直接删除
2) "spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b" 
3) "spring:session:expirations:1635413520000" 
6) "spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b"

更多编程相关知识,请访问:编程视频!!

以上是redis中分散式session不一致性怎麼辦的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除