1、一般实现分布式锁都有哪些方式?

Redis 最普通的分布式锁

也可以使用 Zookeeper 设计锁

2、你们项目中是怎么实现的分布式锁呢?

我们是采用的 Redisson,它是一个具有分布式特性的常用工具类。

3、Redisson 加锁的原理你能说一下吗?

它里面是一个死循环,会一直去 redis 占锁,假设我们没有指定锁的过期时间

  • 它会将过期时间设置为看门狗的默认时间:30 * 1000 ms,也就是 30s,如果加锁成功,它里面就会启动一个定时任务,每隔三分之一看门狗的时间,就会将锁的过期时间重新设置为看门狗的默认时间,也就是会给锁自动续期,这样就不会因为业务时间过长,导致锁自动删掉,等业务执行完,则不会再给当前锁续期了,所以即使不手动释放锁,锁也会在 30s 以后自动删除

如果指定了锁的过期时间则会在占锁的时候,将锁的超时时间设置为我们指定的过期时间。

Zookeeper 是如何实现锁的呢?

有两种方式

创建临时 znode

zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。

创建临时顺序节点

如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 Zookeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。

但是,使用 zk 临时节点会存在另一个问题:由于 zk 依靠 session 定期的心跳来维持客户端,如果客户端进入长时间的 GC,可能会导致 zk 认为客户端宕机而释放锁,让其他的客户端获取锁,但是客户端在 GC 恢复后,会认为自己还持有锁,从而可能出现多个客户端同时获取到锁的情形。

针对这种情况,可以通过 JVM 调优,尽量避免长时间 GC 的情况发生。

这两种分布式锁的实现方式哪种效率比较高?

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。

另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

Session 的原理

  1. 用户第一次访问服务器,进行登录
  2. 登录成功之后,将用户保存到 session 中,浏览器会保存一个 cookie
  3. 以后访问会带上 cookie
  4. 浏览器关闭,会清除会话 cookie
  5. 下次访问,没有 jsessionid,再创建一个

分布式系统下,使用 Session 遇到过什么问题吗?

  1. 遇到了 session 无法同步的问题,因为我们在多个机器上都部署了会员服务,用户如果第一次在 A 机器上登录了,然后第二次用户又负载均衡到了 B 机器,由于用户拿的是 A 机器的 session 信息,B 机器无法识别,所以导致用户在 B 机器上又得登录一遍,同理,如果下次进入 A 机器,那 A 机器也无法识别,用户又得登录。
  2. 还有不同服务之间,子域 session 无法共享的问题

那你们是怎么解决分布式 Session 同步问题的?

首先我们考虑过 Session 复制:

  • 假设用户在 A 机器登录了,然后又进入了 B 机器,此时只要让服务器把数据同步过来,B 机器就能拿到 Session 的全量数据,这样就不用担心负载均衡了
  • 但是这个方案有个很大的问题就是:假设我们有100台 tomcat 服务器,1 台服务器有 1G 空间,那么每次同步,就要把其它 99 台的服务器数据都保存过来,那意味着每个 tomcat 都要有 100G 的内存才能将 session 全量保存过来,所以这个会受到内存限制,服务器没法水平扩展很多。

然后我们还考虑过让客户端存储 Session 数据:

  • 那样服务器就不需要存储 session,用户保存自己的 session 信息到 cookie 中。节省服务端资源
  • 问题太多了,cookie 有长度限制、每次 http 请求,都得携带用户在 cookie 中的完整信息,浪费网络带宽、数据放在 cookie 中也不安全

还考虑过利用 hash 一致性解决:

  • 只需要更改 nginx 配置,根据用户的 ip 进行 hash,只要是同一个 ip 访问,就永远给它定位到同一个服务器,当然还可以结合服务器字段,比如 123 都落到 1 号服务器,456 都落到 2 号服务器
  • 这样数据还是存在服务器中,服务器重启可能导致部分 session 丢失,有些用户需要重新登录,由于服务器是水平扩展,rehash 后 session 重新分布,也会有一部分用户路由不到正确的 session

最终我们选择统一存储:

  • 将 session 统一存储到数据库或redis,这样无论是存、还是取,都能获取到这个用户最新的登录数据
  • 我们使用了 Spring Session

子域 Session 共享如何解决的呢?

给浏览器传递域名信息的时候,指定域名为父域名,这样即使是子域系统发的卡,父域名也能直接使用。

Spring Session 的原理

我现在启动类上的注解@EnableRedisHttpSession 开始说吧

里面导入了 RedisHttpSessionConfiguration.class 的配置,

  1. 这个配置首先给容器中添加了一个组件 RedisOperationsSessionRepository,这个组件的主要作用是,使用 Redis 操作 session,相当于是 session 的增删改查封装类;
  2. 这个配置还继承了 SpringHttpSessionConfiguration ,这个类只要一创建出来,构造器就会初始化CookieSerializer,就是 cookie 的默认序列化;里面还初始化了一个监听器,来监听服务器的停机,以及 session 的序列化、反序列化、等过程;再就是初始化了一个(session 存储过滤器),这个过滤器底层实现了 Filter,相当于每个请求过来都必须经过 Filter 组件,创建的时候,会自动从容器中获取到 SessionRepository(专门存 session 的仓库);最重要的是,它里面重写了**doFilterInternal**,这是 Spring Session 最核心的原理
  3. doFilterInternal 这个方法,首先将当前的sessionRepository(session 的操作类)放到当前请求共享的数据中,将原生的请求对象:请求、响应、当前上下文包装起来,这是一个典型的装饰者模式,然后又将原生的响应对象包装了起来,filterChain 要放行的时候,它将包装之后的对象应用到后面的整个执行链,后面的执行链(Controller)中要使用 session 的时候,以前是从原生的**HttpServletRequest**中获取,而这里的**HttpServletRequest**已经被包装成**SessionRepositoryRequestWrapper**,它底层是从**SessionRepository**中获取的 session,而这个 **SessionRepository**正是我们一开始注入的**RedisOperationsSessionRepository**

所以它的核心原理就是装饰者模式SessionRepositoryRequestWrapper规定怎么获取 session,最终就怎么获取。

Sping Session 模拟了 session 的完整功能,只要浏览器不关,就可以为 session 自动续期,如果浏览器关了,就会正常走 redis 的过期时间

用户购物车,你们存在什么位置?

存在 Redis

购物车为什么存在 Redis

首先考虑,如果用户登录的情况,为了数据的持久化,要显示用户的购物车数据,那应该将数据存储在 MySQL

但是考虑到购物车的读写都是高并发,会给 MySQL 造成非常大的压力,所以我们选择不用数据库

然后我们考虑使用 NoSQL 的数据库,我们考虑过 MongoDB,它比 MySQL 的性能稍微好一点,但是不会为性能带来很大的提升,

所以我们考虑将数据放到Redis里面,Redis 也是一个 NoSQL 的数据库,

选择它,主要是因为它有以下两个优点,一是数据结构好组织,二是 Redis 拥有极高的并发性能

当然还有一个最大的问题就是,我们需要让购物车的数据持久化存储,这个问题,我们可以通过更改 Redis 的持久化策略,让 Redis 的每一条数据都持久化到磁盘里面,就可以解决,

这样,即使 Redis 宕机,下次也可以取出数据,虽然会损失一定的性能,相当于没有那么高的吞吐量了,但是即使这样做,也比 MySQL 快很多

那临时购物车的数据你们存储到哪?

还是存在 Redis

临时购物车为什么也存在 Redis

首先,我们考虑过存到浏览器里面,就是只让客户端存储,后台不存,这样的确能给后台减轻很大的压力,但是缺点也很明显,

但是在大数据的情况下,我们需要经常分析用户的数据,做一些推荐算法,就算是用户没登录,我们也应该推荐商品,存储这些数据,促使用户购买,但是如果后台不存的话,我们就没法分析了,

所以我们需要将所有有价值的数据存到后端,经过上面登录用户的购物车分析,我们还是选择放到 Redis 中

数据模型

为了方便修改购物项,在 Redis 中,我们选择用 hash 存储,第一个值存储商品的 id,第二个值存储商品的具体信息,后来,我们只需要使用类似 Map 的方式,按照 key 查询,就可以快速得到数据,比使用 list 挨个遍历,或者按照购物项的索引找,效率要快的多,