附上Nginx的负载均衡策略

附上Nginx的负载均衡策略:

轮询(默认):

每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

1
2
3
4
upstream backserver { 
server 192.168.0.14;
server 192.168.0.15;
}

指定权重

指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

1
2
3
4
upstream backserver { 
server 192.168.0.14 weight=10;
server 192.168.0.15 weight=10;
}

IP绑定 ip_hash

每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。

1
2
3
4
5
upstream backserver { 
ip_hash;
server 192.168.0.14:88;
server 192.168.0.15:80;
}

fair(第三方)

按后端服务器的响应时间来分配请求,响应时间短的优先分配。

1
2
3
4
5
upstream backserver { 
server 192.168.0.14:88;
server 192.168.0.15:80;
fair;
}

url_hash(第三方)

按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。

1
2
3
4
5
6
7
upstream backserver {
server squid1:3128;
server squid2:3128;

hash $request_uri;
hash_method crc32;
}

(一) Redis 数据类型之字符串String

(一) Redis 数据类型之字符串String

判断key是否存在,返回true / false, 1/0

1
exists key

查看剩余存活时间

※ 获取不存在的key,返回-1

1
ttl key

查看过期key情况

※ redis作为一个内存型的数据库,我们需要对过期key保持关注,从info keyspace中可以看出有多少key没有设置过期时间

1
2
3
info keyspace

eg: db0:keys=10000,expires=3,avg_ttl=583699

set

set key value [ex seconds] [px milliseconds] [nx|xx]

  • ex : 过期 (s)
  • px: 过期 (ms)
  • nx: 不存在则创建
  • xx: 存在则覆盖创建
1
2
3
4
5
6
7
8
9
10
11
12
set key value

set key value ex 10
或者写法为: setex key 10 value

set key value px 1000
或者写法为: psetex key 1000 value

set key value nx
或者写法为: setnx key value

set key value xx

mset

mset, msetnx 批量创建

mset key1 value1 [key2 value2…] #批量创建kv,已存在的会被更新

msetnx key1 value1 [key2 value2…] #批量创建kv,所有key不存在才会创建

1
2
3
4
5
6
127.0.0.1:6379> mset k1 a k2 b k3 c
OK
127.0.0.1:6379> mget k1 k2 k3
1) "a"
2) "b"
3) "c"

del

del key1 [key2 key3…]

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> del k1 k2 k3
(integer) 3
127.0.0.1:6379> del name_xx
(integer) 1
127.0.0.1:6379> mget k1 k2 k3 name_xx name_nx
1) (nil)
2) (nil)
3) (nil)
4) (nil)
5) "dick"

getset

getset key new_value

等同于get+set,执行此命令返回get的结果,然后将该key更新为新的value,下次再get的时候查到的就是新的value。

setrange

setrange key offeset value

对指定下标的字符串进行更新,下标从0开始算起。

keys

keys *

查看所有key

get

get key

使用get获得指定key的value

mget

mget key1 [key2 key3…]

使用mget批量获得指定keys的values

getrange

getrange key start end

从字符串的指定开始结束下标截取字符串返回,下标从0开始算起。

incr

incr key

对指定的key的value值 + 1, 若不存在,初始值为0,incr后为1。字符串无法转换会报错。

decr

decr key

对指定的key的value值 - 1, 若不存在,初始值为0,incr后为-1。字符串无法转换会报错。

incrby

incrby key

增加指定步长,如果key不存在,初始值为0,incrby后为步长。不能转换会报错。

decrby

decrby key 3

增加指定步长,如果key不存在,初始值为0,incrby后为步长的负数。不能转换会报错。

append

append key valu

追加字符串

strlen

strlen key

返回字符串的长度

incrbyfloat

incrbyfloat key 2.5

支持浮点传参

(三)CAP理论和BASE理论

CAP理论

CAP理论说的是:在一个分布式系统中,最多只能满足C、A、P中的两个需求。

CAP的含义:

  • C:Consistency 一致性 同一数据的多个副本是否实时相同。

  • A:Availability 可用性 一定时间内 & 系统返回一个明确的结果 则称为该系统可用。

  • P:Partition tolerance 分区容错性 将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。

++CAP理论告诉我们,在分布式系统中,C、A、P三个条件中我们最多只能选择两个。++

那么问题来了,究竟选择哪两个条件较为合适呢?

对于一个业务系统来说,可用性和分区容错性是必须要满足的两个条件,并且这两者是相辅相成的。业务系统之所以使用分布式系统,主要原因有两个:

  1. 提升整体性能

当业务量猛增,单个服务器已经无法满足我们的业务需求的时候,就需要使用分布式系统,使用多个节点提供相同的功能,从而整体上提升系统的性能,这就是使用分布式系统的第一个原因。

  1. 实现分区容错性

单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。

这说明分区容错性是分布式系统的根本,如果分区容错性不能满足,那使用分布式系统将失去意义。

此外,可用性对业务系统也尤为重要。在大谈用户体验的今天,如果业务系统时常出现“系统异常”、响应时间过长等情况,这使得用户对系统的好感度大打折扣,在互联网行业竞争激烈的今天,相同领域的竞争者不甚枚举,系统的间歇性不可用会立马导致用户流向竞争对手。因此,我们只能通过牺牲一致性来换取系统的可用性和分区容错性。这也就是下面要介绍的BASE理论。

BASE理论

CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性。下面来介绍下BASE理论。

BA:Basic Available 基本可用

整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:
“一定时间”可以适当延长
当举行大促时,响应时间可以适当延长

给部分用户返回一个降级页面
给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。

S:Soft State:柔性状态

同一数据的不同副本的状态,可以不需要实时一致。

E:Eventual Consisstency:最终一致性

同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

酸碱平衡

ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。

Redis使用Lua脚本的注意点

Redis使用Lua脚本的注意点

  1. Lua脚本的bug特别可怕,由于Redis的单线程特点,一旦Lua脚本出现不会返回(不是返回值)得问题,那么这个脚本就会阻塞整个redis实例。

  2. Lua脚本应该尽量短小实现关键步骤即可。(原因同上)

  3. Lua脚本中不应该出现常量Key,这样会导致每次执行时都会在脚本字典中新建一个条目,应该使用全局变量数组KEYS和ARGV, KEYS和ARGV的索引都从1开始

  4. 传递给lua脚本的的键和参数:传递给lua脚本的键列表应该包括可能会读取或者写入的所有键。传入全部的键使得在使用各种分片或者集群技术时,其他软件可以在应用层检查所有的数据是不是都在同一个分片里面。另外集群版redis也会对将要访问的key进行检查,如果不在同一个服务器里面,那么redis将会返回一个错误。(决定使用集群版之前应该考虑业务拆分),参数列表无所谓。

  5. Lua脚本跟单个redis命令和事务段一样都是原子的已经进行了数据写入的lua脚本将无法中断,只能使用SHUTDOWN NOSAVE杀死Redis服务器,所以lua脚本一定要测试好。

如何使用 Redis 实现分布式锁

如何使用 Redis 实现分布式锁

锁是我们在设计和实现大多数系统时绕不过的话题。一旦有竞争条件出现,在没有保护的操作的前提下,可能会出现不可预知的问题。

而现代系统大多为分布式系统,这就引入了分布式锁,要求具有在分布各处的服务上保护资源的能力。

而实现分布式锁,目前大多有以下三种方式:

  • 使用数据库实现。
  • 使用 Redis 等缓存系统实现。
  • 使用 Zookeeper 等分布式协调系统实现。

其中 Redis 简便灵活,高可用分布式,且支持持久化。

设计锁

锁本身是很简单的,就是redis数据库中一个简单的key。建立和释放锁,并保证绝对的安全,是这个锁的设计比较棘手的地方。有两个潜在的陷阱:

  • 应用程序通过网络和redis交互,这意味着从应用程序发出命令到redis结果返回之间会有延迟。这段时间内,redis可能正在运行其他的命令,而redis内数据的状态可能不是你的程序所期待的。如果保证程序中获取锁的线程和其他线程不发生冲突?

  • 如果程序在获取锁后突然crash,而无法释放它?这个锁会一直存在而导致程序进入“饿死”(原文成为“死锁”,感觉不太准确)。

建立锁

可能想到的最简单的方法是“用GET方法检查锁,如果锁不存在,就用SET方式设置一个值”。

这个方法虽然简单,但是不能保证独占锁。

回顾前面所说的第1个陷阱:因为在GET和SET操作之间有延迟,我们没法知道从“发送命令”到“redis服务器返回结果”之间的这段时间内是否有其他线程也去建立锁。当然,这些都在几毫秒之内,发生的可能性相当低。但是如果在一个繁忙的环境中运行着大量的并发线程和命令,重叠的可能性并不是微不足道的。

为了解决这个问题,应该用SETNX命令。SETNX消除了GET命令需要等待返回值的问题,SETNX只有在key不存在时才返回成功。这意味着只有一个线程可以成功运行SETNX命令,而其他线程会失败,然后不断重试,直到它们能建立锁。

释放锁

一旦线程成功执行了SETNX命令,它就建立了锁并且可以基于资源进行工作。工作完成后,线程需要通过删除redis的key来释放这个锁,从而允许其他线程能尽快的获取锁。

尽管如此,也有需要小心的地方!回顾前面说的第2个陷阱:如果线程crash了,它永远都不会删除redis的key,所以这个锁会一直存在,从而导致“饿死”现象。那么如何避免这个问题呢?

锁的存活时间

我们可以给锁加一个存活时间(TTL),这样一旦TTL超时,这个锁的key会被redis自动删除。任何由于线程错误而遗留下来的锁在一个合适的时间之后都会被释放,从而避免了“饿死”。这纯粹是一个安全特性,更有效的方式仍然是确保尽量在线程里面释放锁。

可以通过PEXPIRE命令为Redis的key设置TTL,而且线程里可以通过MULTI/EXEC事务的方式在SETNX命令后立即执行,例如:

1
2
3
4
MULTI
SETNX lock-key
PEXPIRE 10000 lock-key
EXEC

尽管如此,这会产生另外一个问题。PEXPIRE命令没有判断SETNX命令的返回结果,无论如何都会设置key的TTL。如果这个地方无法获取到锁或有异常,那么多个线程每次想获取锁时,都会频繁更新key的TTL,这样会一直延长key的TTL,导致key永远都不会过期。为了解决这个问题,我们需要Redis在一个命令里面处理这个逻辑。我们可以通过Redis脚本的方式来实现。

注意-如果不采用脚本的方式来实现,可以使用Redis 2.6.12之后版本SET命令的PX和NX参数来实现。为了考虑兼容2.6.0之前的版本,我们还是采用脚本的方式来实现。

Redis脚本
由于Redis支持脚本,我们可以写一个Lua脚本在Redis服务端运行多个Redis命令。应用程序通过一条EVALSHA命令就可以调用被Redis服务端缓存的脚本。这里强大的地方在于你的程序只需要运行一条命令(脚本)就可以以事务的方式运行多个redis命令,还能避免并发冲突,因为一个redis脚本同一时刻只能运行一次。

这是Redis里面一个设置带TTL的锁的Lua脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--
-- Set a lock
--
-- KEYS[1] - key
-- KEYS[2] - ttl in ms
-- KEYS[3] - lock content
local key = KEYS[1]
local ttl = KEYS[2]
local content = KEYS[3]

local lockSet = redis.call('setnx', key, content)

if lockSet == 1 then
redis.call('pexpire', key, ttl)
end

return lockSet

从这个脚本可以很清楚的看到,我们通过在锁上只运行PEXPIRE命令就解决了前面提到的“无休止的TTL”问题。

释放锁

原文中还缺少一个释放锁的脚本,如果一直依赖TTL来释放锁,效率会很低。Redis的SET操作文档就提供了一个释放锁的脚本:

1
2
3
4
5
6
if redis.call("get", KEYS[1]) == ARGV[1]
then
return redis.call("del", KEYS[1])
else
return 0
end

应用程序中只要加锁的时候指定一个随机数或特定的value作为key的值,解锁的时候用这个value去解锁就可以了。当然,每次加锁时的value必须要保证是唯一的。

(十四)Redis实现分布式锁方案比较

(十四)Redis实现分布式锁方案比较

SETNX 语义

1
SETNX key value

命令执行时,如果 ==key== 不存在,则设置 key 值为 value(同set);如果 key 已经存在,则不执行赋值操作。并使用不同的返回值标识。

1
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]

==NX== - 仅在 key 不存在时执行赋值操作。命令描述文档 而如下文所述,通过SET的NX选项使用,可同时使用其它选项,如EX/PX设置超时时间,是更好的方式。

SETNX 实现分布式锁

方案1:SETNX + delete

伪代码如下:

1
2
3
setnx lock_a random_value
// do sth
delete lock_a

此实现方式的问题在于:一旦服务获取锁之后,因某种原因挂掉,则锁一直无法自动释放。从而导致死锁。

方案2:SETNX + SETEX

伪代码如下:

1
2
3
4
setnx lock_a random_value
setex lock_a 10 random_value // 10s超时
// do sth
delete lock_a

按需设置超时时间。此方案解决了方案1死锁的问题,但同时引入了新的死锁问题: 如果setnx之后,setex 之前服务挂掉,会陷入死锁。 根本原因为 setnx/setex 分为了两个步骤,非原子操作。

方案3:SET NX PX

伪代码如下:

1
2
3
SET lock_a random_value NX PX 10000 // 10s超时
// do sth
delete lock_a

此方案通过 set 的 NX/PX 选项,将加锁、设置超时两个步骤合并为一个原子操作,从而解决方案1、2的问题。(PX与EX选项的语义相同,差异仅在单位。)
此方案目前大多数 sdk、redis 部署方案都支持,因此是推荐使用的方式。
但此方案也有如下问题:
如果锁被错误的释放(如超时),或被错误的抢占,或因redis问题等导致锁丢失,无法很快的感知到。

SET key randomvalue NX PX

方案4在3的基础上,增加对 value 的检查,只解除自己加的锁。
类似于 CAS,不过是 compare-and-delete。
此方案 redis 原生命令不支持,为保证原子性,需要通过lua脚本实现:。
伪代码如下:

1
2
3
SET lock_a random_value NX PX 10000
// do sth
eval "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock_a random_value

此方案更严谨:即使因为某些异常导致锁被错误的抢占,也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占、错误释放,从而进行特殊处理。

注意事项

超时时间

从上述描述可看出,超时时间是一个比较重要的变量:
超时时间不能太短,否则在任务执行完成前就自动释放了锁,导致资源暴露在锁保护之外。
超时时间不能太长,否则会导致意外死锁后长时间的等待。除非人为接入处理。
因此建议是根据任务内容,合理衡量超时时间,将超时时间设置为任务内容的几倍即可。
如果实在无法确定而又要求比较严格,可以采用定期 setex/expire 更新超时时间实现。

重试

如果拿不到锁,建议根据任务性质、业务形式进行轮询等待。
等待次数需要参考任务执行时间。

与redis事务的比较

setnx 使用更为灵活方案。multi/exec 的事务实现形式更为复杂。
且部分redis集群方案(如codis),不支持multi/exec 事务。

(十)Redis持久化命令

(十)Redis持久化命令

数据持久化

异步AOF

执行一个异步的AOF(append only file)文件重写

1
bgrewriteaof

同步RDB

同步RDB持久化数据到磁盘,同步地将redis中的数据持久化到磁盘

1
save

异步RDB

异步RDB持久化数据到磁盘,异步将redis中的数据持久化到磁盘

1
bgsave

查看上次RDB持久化时间

使用lastsave命令查看上次持久化到磁盘的时间:

1
lastsave

(五)Redis数据类型之Hash

(五)Redis数据类型之Hash

hexists

hexists key field

查看hash类型的key中指定的field是否存在,返回true / false, 1/0

hset

hset key field value [field value …]

可设置单个field, 也可以设置多个值

hsetnx

hsetnx key field value

只有不存在的field才会被创建,若field已存在则不做任何动作

hdel

hdel key field[field2 …]

删除map中指定field的数据, 可以删除多个

hget

hget key field

获取指定field的值

hmget

hmget key field1 [field2 …]

获取指定多个field的值

hgetall

hgetall key

获取指定hashmap的全部field和value

hkeys

hkeys key

获取指定hash类型对象的全部field

hvals

hvals key

获取指定hash类型对象的全部value

hincrby

hincrby key field increment

对HashMap指定的field对应的value做增加操作,increment是整数, increment为负数,则为减少操作,value必须是integer类型。

hincrbyfloat

hincrbyfloat key field increment

对HashMap指定的field对应的value做增加操作,increment是整s数或者浮点数, increment为负数,则为减少操作,value必须是数字类型。

hlen

hlen key

计算field数量

hstrlen

hstrlen key field

获取map中指定field对应value的字符长度

expire

expire key seconds

我们可以看到hash类型没有hsetex hpsetex一类的方法,想对hash对象做过期策略可以使用全局函数expire,单位为秒。

(六)Redis数据类型之Set

(六)Redis数据类型之Set

sadd

sadd key member [member1 …]

给集合内新增成员,若集合不存在则创建集合并新增成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
sadd set one
sadd set two
sadd set three
sadd set four
sadd set five


Docker:0>smembers set
1) "four"
2) "two"
3) "one"
4) "three"
5) "five"

srem

srem key member [member1 …]

移除元素

smove

smove source destination member

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Docker:0>sadd set1 dick joe nick joe
"3"

Docker:0>sadd set2 helen steve frank
"3"

Docker:0>smove set1 set2 joe
"1"

Docker:0>smembers set2
1) "steve"
2) "joe"
3) "frank"
4) "helen"

Docker:0>smembers set1
1) "nick"
2) "dick"

smembers

smembers key

查看成员

1
2
3
4
5
Docker:0>smembers set2
1) "steve"
2) "joe"
3) "frank"
4) "helen"

scard

scard key

返回集合中成员的个数

1
2
3
4
5
 Docker:0>scard set2
"4"

Docker:0>scard set1
"2"

srandmember

srandmember key [count]

从集合中随机返回指定个数的成员

1
2
3
Docker:0>srandmember set2 2
1) "helen"
2) "frank"

sismember

sismember key member

判断是否存在于指定key的集合中

1
2
3
4
5
 Docker:0>smembers set1
1) "nick"
2) "dick"
Docker:0>sismember set1 dick
"1"

spop

spop key

从集合中随机弹出一个成员,返回该成员并从集合中删除该成员

1
2
3
4
5
6
7
8
9
10
11
Docker:0>smembers set2
1) "steve"
2) "joe"
3) "frank"
4) "helen"
Docker:0>spop set2
"joe"
Docker:0>smembers set2
1) "steve"
2) "frank"
3) "helen"

sinter

sinter key [key2 key3 …]

取多个集合的交集,返回这些集合中共同拥有的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Docker:0>smembers setA
1) "C"
2) "A"
3) "F"
4) "B"
5) "D"
6) "E"
Docker:0>smembers setB
1) "E"
2) "D"
3) "B"
4) "C"
5) "F"
Docker:0>smembers setC
1) "D"
2) "C"

Docker:0>sinter setA setB setC
1) "D"
2) "C"

sinterstore

sinterstore destination key [key1 key2 …]

取多个集合的交集∩, 结果存于新的set

1
2
3
4
5
6
Docker:0>sinterstore rest_A_B_C setA setB setC
"2"

Docker:0>smembers rest_A_B_C
1) "D"
2) "C"

sunion

sunion key [key1 key2 …]

求并集∪,相同的成员会被去重

1
2
3
4
5
6
7
Docker:0>sunion setA setB setC
1) "F"
2) "B"
3) "D"
4) "A"
5) "C"
6) "E"

sunionstore

sunionstore destination key [key …]

将多个集合的并集的结果保存为一个新的集合destination ,返回新集合的成员个数。

1
2
3
4
5
6
7
8
9
10
Docker:0>sunionstore union_A_B_C setA setB setC
"6"

Docker:0>smembers union_A_B_C
1) "F"
2) "B"
3) "D"
4) "A"
5) "C"
6) "E"

sdiff

sdiff key [key1 key2 …]

取多个集合的差集,以最左边的为主集合,返回左集合中有而其他集合没有的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Docker:0>smembers setA
1) "C"
2) "A"
3) "F"
4) "B"
5) "D"
6) "E"
Docker:0>smembers setB
1) "E"
2) "D"
3) "B"
4) "C"
5) "F"
Docker:0>smembers setC
1) "D"
2) "C"
※ 集合A 比集合B和集合C多了A这个元素
Docker:0>sdiff setA setB setC
1) "A"

sdiffstore

sdiffstore destination key [key1 key2 …]

将多个集合的差集的结果保存为一个新的集合destination,返回新集合的成员个数

1
2
3
4
5
6

Docker:0>sdiffstore diff_A_B_C setA setB setC
"1"

Docker:0>smembers diff_A_B_C
1) "A"

set 集合结合具体业务场景:

应用场景

  • 抽奖:随机返回指定个数成员
  • 共同好友:取交集
  • 好友推荐:根据标签取交集,交集的成员个数大于某个阈值触发推荐动作

(三) Redis数据类型之List下

rpoplpush

消费列表A的最右边的元素返回,然后追加到列表B的最左边:

rpoplpush source destination

rpoplpush List_A List_B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Docker:0>lrange source 0 -1
1) "1"
2) "2"
3) "3"
Docker:0>lrange destination 0 -1
1) "4"
2) "5"
3) "6"
Docker:0>rpoplpush source destination
"3"

Docker:0>lrange source 0 -1
1) "1"
2) "2"
Docker:0>lrange destination 0 -1
1) "3"
2) "4"
3) "5"
4) "6"

blpop

blpop key timeout

列表左侧查询元素,返回列表的key和左侧第一个元素。若所有查询的列表中都没有元素,则会阻塞等待至设置的timeout秒之后返回空,若在这期间,这些列表新增了元素,则会立刻消费并返回该元素。

brpop

brpop key timeout

类似,从右侧消费。

brpoplpush

brpoplpush source destination timeout
结合brpop和lpush,阻塞消费并将消费到的元素添加至target列表的最左侧: