http

什么是无状态呢?就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。这种无状态的的好处是快速。坏处是假如我们想要把www.zhihu.com/login.htmlwww.zhihu.com/index.html关联起来,必须使用某些手段和工具

cookie和session

cookie是session的一种实现方案

客户端访问服务器的流程如下:

  • 首先,客户端会发送一个http请求到服务器端。

  • 服务器端接受客户端请求后,建立一个session,并发送一个http响应到客户端,这个响应头,其中就包含Set-Cookie头部。该头部包含了sessionId。Set-Cookie格式如下,具体请看Cookie详解

    Set-Cookie: value[; expires=pubDate][; domain=domain][; path=path][; secure]

  • 在客户端发起的第二次请求,假如服务器给了set-Cookie,浏览器会自动在请求头中添加cookie

  • 服务器接收请求,分解cookie,验证信息,核对成功后返回response给客户端

注意

  • cookie只是实现session的其中一种方案。虽然是最常用的,但并不是唯一的方法。禁用cookie后还有其他方法存储,比如放在url中

  • 现在大多都是Session + Cookie,但是只用session不用cookie,或是只用cookie,不用session在理论上都可以保持会话状态。可是实际中因为多种原因,一般不会单独使用

  • 用session只需要在客户端保存一个id,实际上大量数据都是保存在服务端。如果全部用cookie,数据量大的时候客户端是没有那么多空间的。

  • 如果只用cookie不用session,那么账户信息全部保存在客户端,一旦被劫持,全部信息都会泄露。并且客户端数据量变大,网络传输的数据量也会变大

区别

  • session是保存在服务端的,cookie是保存在客户端的

token

token 也称作令牌,由uid+time+sign[+固定参数]

token 的认证方式类似于临时的证书签名, 并且是一种服务端无状态的认证方式, 非常适合于 REST API 的场景. 所谓无状态就是服务端并不会保存身份认证相关的数据。

组成

  • uid: 用户唯一身份标识

  • time: 当前时间的时间戳

  • sign: 签名, 使用 hash/encrypt 压缩成定长的十六进制字符串,以防止第三方恶意拼接

  • 固定参数(可选): 将一些常用的固定参数加入到 token 中是为了避免重复查库

存放

token在客户端一般存放于localStorage,cookie,或sessionStorage中。在服务器一般存于数据库中

参考链接:

来源

为什么需要分布式锁?

主要是由于在单服务器系统我们常用本地锁来避免并发带来的问题,但是,当服务采用集群方式部署时,本地锁无法在多个服务器之间生效,这时候保证数据的一致性就需要分布式锁来实现。

特征

一个相对安全的分布式锁具备什么特征?

  • 互斥性。互斥是锁的基本特征,同一时刻锁只能被一个线程持有,执行临界区操作。

  • 超时释放。通过超时释放,可以避免死锁,防止不必要的线程等待和资源浪费,类似于 MySQL 的 InnoDB 引擎中的 innodblockwait_timeout 参数配置。

  • 可重入性。一个线程在持有锁的情况可以对其再次请求加锁,防止锁在线程执行完临界区操作之前释放。

  • 高性能和高可用。加锁和释放锁的过程性能开销要尽可能的低,同时也要保证高可用,防止分布式锁意外失效。

实现方式

  • Memcached 分布式锁

  • 利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。

  • Zookeeper 分布式锁

  • 利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。ZooKeeper 作为一个专门为分布式应用提供方案的框架,它提供了一些非常好的特性,如 ephemeral 类型的 znode 自动删除的功能,同时 ZooKeeper 还提供 watch 机制,可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。

  • Chubby

  • Google 公司实现的粗粒度分布式锁服务,有点类似于 ZooKeeper,但也存在很多差异。Chubby 通过 sequencer 机制解决了请求延迟造成的锁失效的问题。

  • Redis 分布式锁

  • 基于 Redis 单机实现的分布式锁,其方式和 Memcached 的实现方式类似,利用 Redis 的 SETNX 命令,此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。而基于 Redis 多机实现的分布式锁 Redlock,是 Redis 的作者 antirez 为了规范 Redis 分布式锁的实现,提出的一个更安全有效的实现机制。

redis分布式锁

setnx

最简单的加锁方式就是直接使用 Redis 的 SETNX 指令,该指令只在 key 不存在的情况下,将 key 的值设置为 value,若 key 已经存在,则 SETNX 命令不做任何动作。key 是锁的唯一标识,可以按照业务需要锁定的资源来命名。

image-20210513152055007

为了防止资源被长期占用,所以需要设置一个过期时间

image-20210513152129848

由于setnx和expire不是原子性的操作,所以还是存在这个过期时间没有加上的原因,资源还是被长期占用了

所以可以用redis的扩展命令

image-20210513152721214

  • NX 表示只有当 lock_resource_id 对应的 key 值不存在的时候才能 SET 成功。保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。
  • EX 10 表示这个锁 10 秒钟后会自动过期,业务可以根据实际情况设置这个时间的大小。

但是这种方式仍然不能彻底解决分布式锁超时问题:

  • 锁被提前释放。假如线程 A 在加锁和释放锁之间的逻辑执行的时间过长(或者线程 A 执行过程中被堵塞),以至于超出了锁的过期时间后进行了释放,但线程 A 在临界区的逻辑还没有执行完,那么这时候线程 B 就可以提前重新获取这把锁,导致临界区代码不能严格的串行执行。

  • 锁被误删。假如以上情形中的线程 A 执行完后,它并不知道此时的锁持有者是线程 B,线程 A 会继续执行 DEL 指令来释放锁,如果线程 B 在临界区的逻辑还没有执行完,线程 A 实际上释放了线程 B 的锁。

锁被误删除的解决办法:

就是在加锁时将 value 设置为一个唯一的随机数(或者线程 ID ),释放锁时先判断随机数是否一致,然后再执行释放操作,确保不会错误地释放其它线程持有的锁,除非是锁过期了被服务器自动释放。

但是由于判断 value 和删除 key 是两个独立的操作,并不是原子性的,所以这个地方需要使用 Lua 脚本进行处理,因为 Lua 脚本可以保证连续多个指令的原子性执行。

但是,上面这个处理方法还是没有保证锁可能被提前释放的问题,所以有了下面的这种redisson的处理方式

Redisson

怎么能解决锁被提前释放这个问题呢?

可以利用锁的可重入特性,让获得锁的线程开启一个定时器的守护线程,每 expireTime/3 执行一次,去检查该线程的锁是否存在,如果存在则对锁的过期时间重新设置为 expireTime,即利用守护线程对锁进行“续命”,防止锁由于过期提前释放。

当然业务要实现这个守护进程的逻辑还是比较复杂的,可能还会出现一些未知的问题。

目前互联网公司在生产环境用的比较广泛的开源框架 Redisson 很好地解决了这个问题,非常的简便易用,且支持 Redis 单实例、Redis M-S、Redis Sentinel、Redis Cluster 等多种部署架构。

Redlock

出现这个锁的原因是,上面两种的分布式锁都只是作用在了是加锁时只作用在一个 Redis 节点上,即使 Redis 通过 Sentinel 保证了高可用,但由于 Redis 的复制是异步的,Master 节点获取到锁后在未完成数据同步的情况下发生故障转移,此时其他客户端上的线程依然可以获取到锁,因此会丧失锁的安全性。

参考文章:

0%