Java语言-redis分布式锁实现抢购秒杀系统的方法教程
小标 2018-11-26 来源 : 阅读 1035 评论 0

摘要:本文主要向大家介绍了Java语言-redis分布式锁实现抢购秒杀系统的方法教程,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

本文主要向大家介绍了Java语言-redis分布式锁实现抢购秒杀系统的方法教程,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。


一、使用分布式锁要满足的几个条件:


系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现) 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL) 同步访问(即有很多个进程同事访问同一个共享资源。没有同步访问,谁管你资源竞争不竞争)


二、应用的场景例子


  管理后台的部署架构(多台tomcat服务器+redis【多台tomcat服务器访问一台redis】+mysql【多台tomcat服务器访问一台服务器上的mysql】)就满足使用分布式锁的条件。多台服务器要访问redis全局缓存的资源,如果不使用分布式锁就会出现问题。


从redis获取值N,对数值N进行边界检查,自加1,然后N写回redis中。这种应用场景很常见,像秒杀,全局递增ID、IP访问限制等。以IP访问限制来说,恶意攻击者可能发起无限次访问,并发量比较大,分布式环境下对N的边界检查就不可靠,因为从redis读的N可能已经是脏数据。传统的加锁的做法(如java的synchronized和Lock)也没用,因为这是分布式环境,这个同步问题的救火队员也束手无策。在这危急存亡之秋,分布式锁终于有用武之地了。


  分布式锁可以基于很多种方式实现,比如zookeeper、redis...。不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。


  这里主要讲如何用redis实现分布式锁。


三、使用redis的setNX命令实现分布式锁


1、实现的原理


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


2、基本命令解析


1)setNX(SETifNoteXists)


语法:


SETNX key value

   

将key的值设为value,当且仅当key不存在。


若给定的key已经存在,则SETNX不做任何动作。


SETNX是『SET if Not eXists』(如果不存在,则 SET)的简写


返回值:


  设置成功,返回1。


  设置失败,返回0。


redis> EXISTS job                # job 不存在

(integer) 0

 

redis> SETNX job "programmer"    # job 设置成功

(integer) 1

 

redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败

(integer) 0

 

redis> GET job                   # 没有被覆盖

"programmer"

   

所以我们使用执行下面的命令


SETNX lock.foo <current 1="" lock="" time="" timeout="" unix=""> </current>

   

如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。


如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。


2)getSET


语法:


GETSET key value

   

  将给定key的值设为value,并返回key的旧值(old value)。


  当key存在但不是字符串类型时,返回一个错误。


返回值:


  返回给定key的旧值。


  当key没有旧值时,也即是,key不存在时,返回nil。


3)get


语法:


GET key

   

返回值:


  当key不存在时,返回nil,否则,返回key的值。


  如果key不是字符串类型,那么返回一个错误


四、解决死锁


上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?


我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。

   

  发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次(讲道理,删除锁的操作应该是锁拥有这执行的,这里只需要等它超时即可),当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:


C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。 

C1 发送DEL lock.foo 

C1 发送SETNX lock.foo 并且成功了。 

C2 发送DEL lock.foo 

C2 发送SETNX lock.foo 并且成功了。 

这样一来,C1,C2都拿到了锁!问题大了!

   

  幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:


C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0

C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。 

反之,如果已超时,C3通过下面的操作来尝试获得锁: 

GETSET lock.foo <current 1="" lock="" time="" timeout="" unix=""> 

通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。 

如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。 </current>

   

  注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。


五、代码实现


expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放


timeoutMsecs 锁等待超时,防止线程饥饿,永远没有入锁执行代码的机会


注意:项目里面需要先搭建好redis的相关配置

package test.miaosha;

 

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.dao.DataAccessException;

import org.springframework.data.redis.connection.RedisConnection;

import org.springframework.data.redis.core.RedisCallback;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.StringRedisSerializer;

 

public class RedisLock {

 

    private static Logger logger = LoggerFactory.getLogger(RedisLock.class);

 

    private RedisTemplate<string,object> redisTemplate;

 

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

 

    /**

     * Lock key path.

     */

    private String lockKey;

 

    /**

     * 锁超时时间,防止线程在入锁以后,无限的执行等待

     */

    private int expireMsecs = 60 * 1000;

 

    /**

     * 锁等待时间,防止线程饥饿

     */

    private int timeoutMsecs = 10 * 1000;

 

    private volatile boolean locked = false;

 

    /**

     * Detailed constructor with default acquire timeout 10000 msecs and lock

     * expiration of 60000 msecs.

     *

     * @param lockKey

     *            lock key (ex. account:1, ...)

     */

    public RedisLock(RedisTemplate<string,object> redisTemplate, String lockKey) {

        this.redisTemplate = redisTemplate;

        this.lockKey = lockKey + "_lock";

    }

 

    /**

     * Detailed constructor with default lock expiration of 60000 msecs.

     *

     */

    public RedisLock(RedisTemplate<string,object> redisTemplate, String lockKey, int timeoutMsecs) {

        this(redisTemplate, lockKey);

        this.timeoutMsecs = timeoutMsecs;

    }

 

    /**

     * Detailed constructor.

     *

     */

    public RedisLock(RedisTemplate<string,object> redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {

        this(redisTemplate, lockKey, timeoutMsecs);

        this.expireMsecs = expireMsecs;

    }

 

    /**

<div class="li    

   

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注编程语言JAVA频道!


本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 1 不喜欢 | 0
看完这篇文章有何感觉?已经有1人表态,100%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程