目前几乎很多大型网站及应用都是分布式部署的, 分布式场景中的数据一致性问题一直是一个比较重要的话题. 分布式的 CAP 理论告诉我们 "任何一个分布式系统都无法同时满足一致性 (Consistency), 可用性(Availability) 和分区容错性(Partition tolerance), 最多只能同时满足两项." 所以, 很多系统在设计之初就要对这三者做出取舍. 在互联网领域的绝大多数的场景中, 都需要牺牲强一致性来换取系统的高可用性, 系统往往只需要保证 "最终一致性", 只要这个最终时间是在用户可以接受的范围内即可.
在很多场景中, 我们为了保证数据的最终一致性, 需要很多的技术方案来支持, 比如分布式事务, 分布式锁等.
选用 Redis 实现分布式锁原因
Redis 有很高的性能
Redis 命令对此支持较好, 实现起来比较方便
使用命令介绍
- SETNX
- SETNX key val
当且仅当 key 不存在时, set 一个 key 为 val 的字符串, 返回 1; 若 key 存在, 则什么都不做, 返回 0.
- expire
- expire key timeout
为 key 设置一个超时时间, 单位为 second, 超过这个时间锁会自动释放, 避免死锁.
- delete
- delete key
删除 key
在使用 Redis 实现分布式锁的时候, 主要就会使用到这三个命令.
实现
使用的是 jedis 来连接 Redis.
实现思想
获取锁的时候, 使用 setnx 加锁, 并使用 expire 命令为锁添加一个超时时间, 超过该时间则自动释放锁, 锁的 value 值为一个随机生成的 UUID, 通过此在释放锁的时候进行判断.
获取锁的时候还设置一个获取的超时时间, 若超过这个时间则放弃获取锁.
释放锁的时候, 通过 UUID 判断是不是该锁, 若是该锁, 则执行 delete 进行锁释放.
分布式锁的核心代码如下:
- import Redis.clients.jedis.Jedis;
- import Redis.clients.jedis.JedisPool;
- import Redis.clients.jedis.Transaction;
- import Redis.clients.jedis.exceptions.JedisException;
- import java.util.List;
- import java.util.UUID;
- /**
- * Created by liuyang on 2017/4/20.
- */
- public class DistributedLock {
- private final JedisPool jedisPool;
- public DistributedLock(JedisPool jedisPool) {
- this.jedisPool = jedisPool;
- }
- /**
- * 加锁
- * @param locaName 锁的 key
- * @param acquireTimeout 获取超时时间
- * @param timeout 锁的超时时间
- * @return 锁标识
- */
- public String lockWithTimeout(String locaName,
- long acquireTimeout, long timeout) {
- Jedis conn = null;
- String retIdentifier = null;
- try {
- // 获取连接
- conn = jedisPool.getResource();
- // 随机生成一个 value
- String identifier = UUID.randomUUID().toString();
- // 锁名, 即 key 值
- String lockKey = "lock:" + locaName;
- // 超时时间, 上锁后超过此时间则自动释放锁
- int lockExpire = (int)(timeout / 1000);
- // 获取锁的超时时间, 超过这个时间则放弃获取锁
- long end = System.currentTimeMillis() + acquireTimeout;
- while (System.currentTimeMillis() <end) {
- if (conn.setnx(lockKey, identifier) == 1) {
- conn.expire(lockKey, lockExpire);
- // 返回 value 值, 用于释放锁时间确认
- retIdentifier = identifier;
- return retIdentifier;
- }
- // 返回 - 1 代表 key 没有设置超时时间, 为 key 设置一个超时时间
- if (conn.ttl(lockKey) == -1) {
- conn.expire(lockKey, lockExpire);
- }
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- } catch (JedisException e) {
- e.printStackTrace();
- } finally {
- if (conn != null) {
- conn.close();
- }
- }
- return retIdentifier;
- }
- /**
- * 释放锁
- * @param lockName 锁的 key
- * @param identifier 释放锁的标识
- * @return
- */
- public boolean releaseLock(String lockName, String identifier) {
- Jedis conn = null;
- String lockKey = "lock:" + lockName;
- boolean retFlag = false;
- try {
- conn = jedisPool.getResource();
- while (true) {
- // 监视 lock, 准备开始事务
- conn.watch(lockKey);
- // 通过前面返回的 value 值判断是不是该锁, 若是该锁, 则删除, 释放锁
- if (identifier.equals(conn.get(lockKey))) {
- Transaction transaction = conn.multi();
- transaction.del(lockKey);
- List<Object> results = transaction.exec();
- if (results == null) {
- continue;
- }
- retFlag = true;
- }
- conn.unwatch();
- break;
- }
- } catch (JedisException e) {
- e.printStackTrace();
- } finally {
- if (conn != null) {
- conn.close();
- }
- }
- return retFlag;
- }
}
测试
下面就用一个简单的例子测试刚才实现的分布式锁.
例子中使用 50 个线程模拟秒杀一个商品, 使用 -- 运算符来实现商品减少, 从结果有序性就可以看出是否为加锁状态.
模拟秒杀服务, 在其中配置了 jedis 线程池, 在初始化的时候传给分布式锁, 供其使用.
- import Redis.clients.jedis.JedisPool;
- import Redis.clients.jedis.JedisPoolConfig;
- /**
- * Created by liuyang on 2017/4/20.
- */
- public class Service {
- private static JedisPool pool = null;
- static {
- JedisPoolConfig config = new JedisPoolConfig();
- // 设置最大连接数
- config.setMaxTotal(200);
- // 设置最大空闲数
- config.setMaxIdle(8);
- // 设置最大等待时间
- config.setMaxWaitMillis(1000 * 100);
- // 在 borrow 一个 jedis 实例时, 是否需要验证, 若为 true, 则所有 jedis 实例均是可用的
- config.setTestOnBorrow(true);
- pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
- }
- DistributedLock lock = new DistributedLock(pool);
- int n = 500;
- public void seckill() {
- // 返回锁的 value 值, 供释放锁时候进行判断
- String indentifier = lock.lockWithTimeout("resource", 5000, 1000);
- System.out.println(Thread.currentThread().getName() + "获得了锁");
- System.out.println(--n);
- lock.releaseLock("resource", indentifier);
- }
}
- // 模拟线程进行秒杀服务
- public class ThreadA extends Thread {
- private Service service;
- public ThreadA(Service service) {
- this.service = service;
- }
- @Override
- public void run() {
- service.seckill();
- }
- }
- public class Test {
- public static void main(String[] args) {
- Service service = new Service();
- for (int i = 0; i < 50; i++) {
- ThreadA threadA = new ThreadA(service);
- threadA.start();
- }
- }
}
结果如下, 结果为有序的.
若注释掉使用锁的部分
- public void seckill() {
- // 返回锁的 value 值, 供释放锁时候进行判断
- //String indentifier = lock.lockWithTimeout("resource", 5000, 1000);
- System.out.println(Thread.currentThread().getName() + "获得了锁");
- System.out.println(--n);
- //lock.releaseLock("resource", indentifier);
}
从结果可以看出, 有一些是异步进行的.
在分布式环境中, 对资源进行上锁有时候是很重要的, 比如抢购某一资源, 这时候使用分布式锁就可以很好地控制资源.
当然, 在具体使用中, 还需要考虑很多因素, 比如超时时间的选取, 获取锁时间的选取对并发量都有很大的影响, 上述实现的分布式锁也只是一种简单的实现, 主要是一种思想.
下一次我会使用 zookeeper 实现分布式锁, 使用 zookeeper 的可靠性是要大于使用 Redis 实现的分布式锁的, 但是相比而言, Redis 的性能更好!
最后
大家觉得不错可以点个赞在关注下我, 刚刚入驻, 以后还会分享更多文章!
来源: https://juejin.im/post/5c94e74c6fb9a070b33c5539