前段时间,我在的项目组准备做一个类似美团外卖的拼手气红包【第X个领取的人红包最大】,基本功能实现后,就要考虑这一操作在短时间内多个用户争抢同一资源的并发问题了,类似于很多应用如淘宝、京东的秒杀活动场景。所谓的秒杀就是多个线程对资源进行操作,而实现秒杀,就必须控制线程对资源的争抢。

传统方法

  而最传统简单暴力的方法就是在秒杀的业务关键代码块外用Java的synchronized关键字锁住,但这种方式下的锁粒度比较高,比如两个线程同时执行秒杀方法,这两个线程操作的是不同的商品,从业务上讲应该是可以同时进行的,而两个线程会去争抢同一个锁,这是没必要的,而且synchronized是线程同步锁,只允许一个进程的一个线程访问,分布式场景下无法控制同步。这时候,分布式锁上场了。

场景

  论分布式锁,查阅了很多资料,有很多方法可以实现,如zookeeper、redis等等,而他们的共同点都是通过状态值来标识锁,进而通过状态值来实现锁的占用与释放。比如现在有一个秒杀场景,db有一张表,对应有商品ID和库存,秒杀成功库存-1,现有500个线程秒杀商品1,另有500个线程秒杀商品2。通常具有秒杀场景的业务系统都比较复杂,承载的业务量非常巨大,并发量也很高。这样的系统往往采用分布式的架构来均衡负载。那么这1000个并发就会是从不同的地方过来,商品库存就是共享的资源,也是这1000个并发争抢的资源,这个时候我们需要将并发互斥管理起来,把和商品ID相关的字符串作为状态值来标识锁,这样就只有争抢同一商品的线程互斥,不会导致所以线程互斥。

  下面介绍下redis分布式锁的实现。

实现原理   

  Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的setnx命令可以方便的实现分布式锁。

三个命令  

   setNx key value  [key不存在时设置对应value;key已存在不做任何操作;意‘set if not exists’]

   get  [key不存在返回nil;key已存在返回值]

getSet  [设置key值为value,并返回key的旧值]

上代码就是一把梭[锁]

 package com.pagoda.eshop.customer.redis.lock;

 /**
* 分布式锁接口
* @Author: 小海
* @Description:
* @Date: Create in 17:28 2017/11/8
*/
public interface IRedisLock { /**
* 获取锁
* @param lockKey
* @return
* @throws InterruptedException
*/
boolean lock(String lockKey) throws InterruptedException; /**
* 释放锁
* @param lockKey
*/
void unlock(String lockKey);
}
 package com.pagoda.eshop.customer.redis.lock.impl;

 import com.pagoda.eshop.customer.redis.lock.IRedisLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.JedisCluster; /**
* 基于redis实现分布式锁
* 备注-> https://www.cnblogs.com/novaCN/p/6417330.html[类模板/方法模板的配置-电商代码规范]
* @Author: 小海
* @Description:
* @Date: Create in 17:28 2017/11/8
*/
@Service
public class RedisLock implements IRedisLock{ private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired
private JedisCluster jedis; private String lockKey; private static final int DEFAULT_LOOP_INTERVAL_MILLIS = 100; /**
* 锁超时时间,防止线程在入锁以后,无限的执行等待
*/
private int expireMsecs = 5 * 1000; /**
* 锁等待时间,防止线程饥饿死锁
*/
private int timeoutMsecs = 10 * 1000; /**
* 锁标识
*/
private volatile boolean locked = false; /**
* 锁key后缀
*/
private static final String LOCKKEY_SUFFIX = ":lock"; private String get(final String key) {
Object obj = null;
try {
obj = jedis.get(key);
} catch (Exception e) {
logger.error("get redis error, key : ", key);
}
return obj == null ? null : obj.toString();
} /**
* 若key不存在,将key的值设为value,并返回true
* 若key已经存在,则setnx不做任何动作,并返回false
* @param key
* @param value
* @return
*/
private boolean setNX(final String key, final String value) {
Object obj = null;
try {
obj = jedis.setnx(key, value);
} catch (Exception e) {
logger.error("setNX redis error, key : ", key);
}
return ((Long) obj).intValue() == 0 ? false : true;
} /**
* 设置现在的锁到期时间并返回上一个锁到期时间
* @param key
* @param value
* @return 上一个锁的到期时间
*/
private String getSet(final String key, final String value) {
Object obj = null;
try {
obj = jedis.getSet(key, value);
} catch (Exception e) {
logger.error("getSet redis error, key : ", key);
}
return obj == null ? null : (String) obj;
} /**
* 获取锁
*
* 实现思路:
* 主要是使用了redis的setnx命令,缓存了锁
* reids缓存的key是锁的key,所有的共享,value是锁的到期时间
*
* 执行过程:
* 1.通过setnx尝试设置某个key的值,若锁不存在,则返回true,成功获得锁
* 2.若锁已经存在,则通过get获取锁的到期时间,和当前时间比较,超时的话,则通过getset设置新的值并返回上一个线程锁的到期时间
* 3.若通过get和getset获取到的线程锁的到期时间一致的话,则返回true,成功获得锁
* 4.若无法满足1或3的条件,则睡眠一小段时间,一定时间内循环1~3操作,尝试加锁
* 5.若超出锁等待时间,则返回false,获取锁失败
*
* @return 若获得锁,返回true;若执行超时,返回false
* @throws InterruptedException
*/
@Override
public boolean lock(String lockKey) throws InterruptedException {
lockKey = lockKey + LOCKKEY_SUFFIX;
int timeout = timeoutMsecs;
while (timeout >= 0) {
long expires = System.currentTimeMillis() + expireMsecs + 1;
// 对加锁做时效性检测,设置锁到期时间
String expiresStr = String.valueOf(expires);
if (this.setNX(lockKey, expiresStr)) {
// 成功获得锁
locked = true;
return true;
}
// 当前锁的到期时间
String currentValueStr = this.get(lockKey);
// 若锁已超时[获取锁的客户端执行时间过长,进程被kill掉,或因为其他异常崩溃,导致无法释放锁,就会造成死锁],则重新加锁
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 如果多个线程同时走到这里,但是走到这里时每个线程拿到的oldValueStr肯定不可能一样
String oldValueStr = this.getSet(lockKey, expiresStr); // 如果多个线程同时走到这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 成功获得锁
locked = true;
return true;
}
}
timeout -= DEFAULT_LOOP_INTERVAL_MILLIS;
Thread.sleep(DEFAULT_LOOP_INTERVAL_MILLIS);
}
return false;
} /**
* 释放锁
*/
@Override
public void unlock(String lockKey) {
if (locked) {
jedis.del(lockKey);
locked = false;
}
}
}

===============后续

致谢:感谢您的阅读!一些问题请跳转自 http://www.cnblogs.com/0201zcr/p/5942748.html

Redis分布式锁的实现的更多相关文章

  1. 利用redis分布式锁的功能来实现定时器的分布式

    文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...

  2. Redis分布式锁

    Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...

  3. redis分布式锁和消息队列

    最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...

  4. redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  5. spring boot redis分布式锁

    随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁.分布式锁的实现有很多种,比如基于数据库. zookeeper 等,本文主要介绍使用 Redis 做分布式锁的方式,并封装成spring b ...

  6. Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  7. Redis分布式锁---完美实现

    这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...

  8. redis分布式锁实践

    分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于jvm的锁无法满足多实例中锁的需求,本篇将讲下redis如何通过Lua脚本实现分布式锁,不同于网上的redission,完全是手动实现的 我 ...

  9. Redis分布式锁的try-with-resources实现

    Redis分布式锁的try-with-resources实现 一.简介 在当今这个时代,单体应用(standalone)已经很少了,java提供的synchronized已经不能满足需求,大家自然 而 ...

  10. 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

随机推荐

  1. kubernetes label 标签使用

    查看node的标签kubectl get node --show-labels 删除标签kubectl label nodes node5 storagenode- 设置标签kubectl label ...

  2. 分析占用了大量CPU处理时间的java进程中的进程

    分析占用了大量 CPU 处理时间的是Java 进程中哪个线程 下面是详细步骤: 1. 首先确定进程的 ID ,可以使用 jps -v 或者 top 命令直接查看 2. 查看该进程中哪个线程占用大量 C ...

  3. Docker技术入门与实战 第二版-学习笔记-10-Docker Machine 项目-1-cli

    Docker Machine 是 Docker 官方编排(Orchestration)项目之一,负责在多种平台上快速安装 Docker 环境 Docker Machine是一种工具,它允许你在虚拟主机 ...

  4. canal数据同步目录

    我们公司对于数据同步有以下需求 1.多个mysql库中有一些基础表需要数据统一,mysql跨库同步 2.mysql热数据加载到redis 3.全文检索需要mysql同步到es 4.数据变更是附属的其它 ...

  5. Vue复选框的全选

    <!DOCTYPE html><html>    <head>        <meta charset="utf-8">      ...

  6. C++ 虚函数的使用

    虚函数是C++中用于实现多态(polymorphism)的机制.核心理念就是通过基类访问派生类定义的函数.假设我们有下面的类层次: #include <iostream> using na ...

  7. scapy学习笔记(4)简单的sniffing 嗅探

    转载请注明:@小五义:http://www.cnblogs/xiaowuyi 利用sniff命令进行简单的嗅探,可以抓到一些简单的包.当不指定接口时,将对每一个接口进行嗅探,当指定接口时,仅对该接口进 ...

  8. 在main函数前后执行的函数之 C语言

    在gcc中,可以使用attribute关键字,声明constructor和destructor,来指定了函数在main之前或之后运行,代码如下: #include <stdio.h> __ ...

  9. 详细解读Spark的数据分析引擎:Spark SQL

    一.spark SQL:类似于Hive,是一种数据分析引擎 什么是spark SQL? spark SQL只能处理结构化数据 底层依赖RDD,把sql语句转换成一个个RDD,运行在不同的worker上 ...

  10. AT24C02跨页写数据

    AT24C02 EEPROM的写数据分为:字节写数据模式和页写数据模式:字节写就是一个地址一个数据的写,页写是连续写数据,一个地址多个数据的写,但是页写不能自动跨页,如果超出一页长度,超出的数据会覆盖 ...