先来探视一些redis的中坚命令: SETNX key value
要是key海市蜃楼,就设置key对应字符串value。在此种气象下,该命令和SET雷同。当key已经存在时,就不做此外操作。SETNX是”SET
if Not eXists”。 expire KEY seconds
设置key的超时时间。假如key已过期,将会被电动删除。 del KEY 删除key
由于作者的贯彻只用到这多个指令,就只介绍那八个指令,更加多的通令以至redis的特色和平运动用,能够参见redis官方网站。

/** * 加锁 * 使用方式为: * lock(); * try{ * executeMethod(); * }finally{ * unlock(); * } * @param timeout timeout的时间范围内轮询锁 * @param expire 设置锁超时时间 * @return 成功 or 失败 */ public boolean lock(long timeout,int expire){ long nanoTime = System.nanoTime(); timeout *= MILLI_NANO_TIME; try { //在timeout的时间范围内不断轮询锁 while (System.nanoTime() - nanoTime  timeout) { //锁不存在的话,设置锁并设置锁过期时间,即加锁 if (this.redisClient.setnx(this.key, LOCKED) == 1) { this.redisClient.expire(key, expire);//设置锁过期时间是为了在没有释放 //锁的情况下锁过期后消失,不会造成永久阻塞 this.lock = true; return this.lock; } System.out.println("出现锁等待"); //短暂休眠,避免可能的活锁 Thread.sleep(3, RANDOM.nextInt(30)); } } catch (Exception e) { throw new RuntimeException("locking error",e); } return false; } public void unlock() { try { if(this.lock){ redisClient.delKey(key);//直接删除 } } catch (Throwable e) { } }

刚刚提到过,完毕秒杀的关键点是调整线程对能源的掠夺,依据基本的线程知识,可以不加构思的想到上面包车型大巴局部措施:
1、秒杀在本领层面包车型客车思梅止渴应该正是叁个情势,在这里个措施里只怕的操作是将物品仓库储存-1,将货品步向客商的购物车等等,在不构思缓存的动静下相应是要操作数据库的。那么最简便直接的兑现就是在这里个办法上加上synchronized关键字,通俗的讲就是锁住整个艺术;
2、锁住整个艺术那些政策轻易方便,可是有如某些狠毒。可以稍稍优化一下,只锁住秒杀的代码块,譬如写数据库的一些;
3、既然有现身难点,那本人就让他“不并发”,将有着的线程用叁个类别管理起来,使之成为串行操作,自然不会有现身难题。

上述SeckillInterface接口的完毕类,即秒杀的具体落到实处:

地点所述的法子都是实用的,然而都倒霉。为啥?第一和第三种办法本质上是“加锁”,可是锁粒度依旧相比较高。什么意思?试想一下,假诺五个线程同不常候试行秒杀方法,那三个线程操作的是差异的商品,从作业上讲应该是足以并且打开的,不过假诺利用第一二种方法,那多个线程也会去争抢同一个锁,那其实是不要求的。第二种办法也远非缓慢解决地点说的难点。

实际的落实

talk is cheap,show me the code

遍布式锁是决定布满式系统之间联合访谈分享财富的一种方式。在遍及式系统中,日常须求协和他们的动作。假若不一样的连串恐怕同一个系统的不等主机之间分享了三个或一组能源,那么访问那个能源的时候,往往供给互斥来幸免相互郁闷来保险一致性,在此种情况下,便供给动用到遍及式锁。

多少个评释定义:

有的大概的实现

public interface SeckillInterface {/***现在暂时只支持在接口方法上注解*/ //cacheLock注解可能产生并发的方法 @CacheLock(lockedPrefix="TEST_PREFIX") public void secKill(String userID,@LockedObject Long commidityID);//最简单的秒杀方法,参数是用户ID和商品ID。可能有多个线程争抢一个商品,所以商品ID加上LockedObject注解}

源码酒店

何为布满式锁

先定义八个接口,接口里定义了多个秒杀方法:

public class SecKillImpl implements SeckillInterface{ static MapLong, Long inventory ; static{ inventory = new HashMap(); inventory.put(10000001L, 10000l); inventory.put(10000002L, 10000l); } @Override public void secKill(String arg1, Long arg2) { //最简单的秒杀,这里仅作为demo示例 reduceInventory(arg2); } //模拟秒杀操作,姑且认为一个秒杀就是将库存减一,实际情景要复杂的多 public Long reduceInventory(Long commodityId){ inventory.put(commodityId,inventory.get(commodityId) - 1); return inventory.get(commodityId); }}

上述的代码是框架性的代码,未来来说学如何行使方面包车型地铁大致框架来写二个秒杀函数。

@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface LockedComplexObject { String field() default "";//含有成员变量的复杂对象中需要加锁的成员变量,如一个商品对象的商品ID}
public class CacheLockInterceptor implements InvocationHandler{ public static int ERROR_COUNT = 0; private Object proxied; public CacheLockInterceptor(Object proxied) { this.proxied = proxied; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { CacheLock cacheLock = method.getAnnotation(CacheLock.class); //没有cacheLock注解,pass if(null == cacheLock){ System.out.println("no cacheLock annotation"); return method.invoke(proxied, args); } //获得方法中参数的注解 Annotation[][] annotations = method.getParameterAnnotations(); //根据获取到的参数注解和参数列表获得加锁的参数 Object lockedObject = getLockedObject(annotations,args); String objectValue = lockedObject.toString(); //新建一个锁 RedisLock lock = new RedisLock(cacheLock.lockedPrefix(), objectValue); //加锁 boolean result = lock.lock(cacheLock.timeOut(), cacheLock.expireTime()); if(!result){//取锁失败 ERROR_COUNT += 1; throw new CacheLockException("get lock fail"); } try{ //加锁成功,执行方法 return method.invoke(proxied, args); }finally{ lock.unlock();//释放锁 } } /** * * @param annotations * @param args * @return * @throws CacheLockException */ private Object getLockedObject(Annotation[][] annotations,Object[] args) throws CacheLockException{ if(null == args || args.length == 0){ throw new CacheLockException("方法参数为空,没有被锁定的对象"); } if(null == annotations || annotations.length == 0){ throw new CacheLockException("没有被注解的参数"); } //不支持多个参数加锁,只支持第一个注解为lockedObject或者lockedComplexObject的参数 int index = -1;//标记参数的位置指针 for(int i = 0;i  annotations.length;i++){ for(int j = 0;j  annotations[i].length;j++){ if(annotations[i][j] instanceof LockedComplexObject){//注解为LockedComplexObject index = i; try { return args[i].getClass().getField(((LockedComplexObject)annotations[i][j]).field()); } catch (NoSuchFieldException | SecurityException e) { throw new CacheLockException("注解对象中没有该属性" + ((LockedComplexObject)annotations[i][j]).field()); } } if(annotations[i][j] instanceof LockedObject){ index = i; break; } } //找到第一个后直接break,不支持多参数加锁 if(index != -1){ break; } } if(index == -1){ throw new CacheLockException("请指定被锁定参数"); } return args[index]; }}

政工场景

这两日在类型中相见了犹如“秒杀”的政工场景,在本篇博客中,作者将用多个特别轻易的demo,演讲达成所谓“秒杀”的基本思路。

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface CacheLock { String lockedPrefix() default "";//redis 锁key的前缀 long timeOut() default 2000;//轮询锁的时间 int expireTime() default 1000;//key在redis里存在的时间,1000S}

LockedComplexObject也是参数级的评释,用于表明自定义类型的参数:

最要害的RedisLock类中的lock方法和unlock方法:

@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface LockedObject { //不需要值}

CacheLockInterceptor实现InvocationHandler接口,在invoke方法中收获评释的不二秘诀和参数,在奉行声明的情势前加锁,实行被解说的方法后释放锁:

这便是说怎样将锁调控在更加细的粒度上吧?能够假造为每一个商品设置两个互斥锁,以和商品ID相关的字符串为独一标志,那样就足以成功唯有争抢同一件商品的线程互斥,不会促成全数的线程互斥。分布式锁刚好能够协理大家减轻这几个主题素材。

cachelock是方法级的注释,用于表明会发生并发难点的章程:

在不利的预想下,应该每一个商品的仓库储存都减削了500,在数十三遍试验后,实际境况切合预期。借使不利用锁机制,会鬼使神差仓库储存收缩499,498的图景。
这里运用了动态代理的议程,利用注明和反光机制得到遍布式锁ID,举行加锁和释放锁操作。当然也能够向来在章程开展这几个操作,接纳动态代理也是为着能够将锁操作代码聚焦在代理中,便于维护。
常常秒杀场景发生在web项目中,能够伪造选用spring的AOP天性将锁操作代码置于切面中,当然AOP本质上也是动态代理。

@Test public void testSecKill(){ int threadCount = 1000; int splitPoint = 500; CountDownLatch endCount = new CountDownLatch(threadCount); CountDownLatch beginCount = new CountDownLatch(1); SecKillImpl testClass = new SecKillImpl(); Thread[] threads = new Thread[threadCount]; //起500个线程,秒杀第一个商品 for(int i= 0;i  splitPoint;i++){ threads[i] = new Thread(new Runnable() { public void run() { try { //等待在一个信号量上,挂起 beginCount.await(); //用动态代理的方式调用secKill方法 SeckillInterface proxy = (SeckillInterface) Proxy.newProxyInstance(SeckillInterface.class.getClassLoader(), new Class[]{SeckillInterface.class}, new CacheLockInterceptor(testClass)); proxy.secKill("test", commidityId1); endCount.countDown(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); threads[i].start(); } //再起500个线程,秒杀第二件商品 for(int i= splitPoint;i  threadCount;i++){ threads[i] = new Thread(new Runnable() { public void run() { try { //等待在一个信号量上,挂起 beginCount.await(); //用动态代理的方式调用secKill方法 SeckillInterface proxy = (SeckillInterface) Proxy.newProxyInstance(SeckillInterface.class.getClassLoader(), new Class[]{SeckillInterface.class}, new CacheLockInterceptor(testClass)); proxy.secKill("test", commidityId2); //testClass.testFunc("test", 10000001L); endCount.countDown(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); threads[i].start(); } long startTime = System.currentTimeMillis(); //主线程释放开始信号量,并等待结束信号量,这样做保证1000个线程做到完全同时执行,保证测试的正确性 beginCount.countDown(); try { //主线程等待结束信号量 endCount.await(); //观察秒杀结果是否正确 System.out.println(SecKillImpl.inventory.get(commidityId1)); System.out.println(SecKillImpl.inventory.get(commidityId2)); System.out.println("error count" + CacheLockInterceptor.ERROR_COUNT); System.out.println("total cost " + (System.currentTimeMillis() - startTime)); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }

如法泡制秒杀场景,1000个线程来争抢七个商品:

内需思量的难题

1、用什么操作redis?幸好redis已经提供了jedis顾客端用于java应用程序,直接调用jedis
API就可以。
2、怎么贯彻加锁?“锁”其实是贰个空洞的定义,将那些抽象概念变为具体的东西,即是贰个仓库储存在redis里的key-value对,key是于商品ID相关的字符串来独一标志,value其实并不根本,因为假若这几个独一的key-value存在,就象征那个商品已经上锁。
3、怎么样释放锁?既然key-value对存在就意味着上锁,那么自由锁就自然是在redis里删除key-value对。
4、梗塞还是非阻塞?我利用了绿灯式的兑现,若线程开掘已经上锁,会在特按期期内轮询锁。
5、如什么地点理至极意况?比方三个线程把贰个货物上了锁,可是由于各样缘由,未有做到操作,自然未有释放锁,那些情景笔者加入了锁超机会制,利用redis的expire命令为key设置超时时间长度,过了晚点时间redis就能够将这一个key自动删除,即抑遏释放锁。

大家来假使五个最简便易行的秒杀场景:数据Curry有一张表,column分别是商品ID,和商品ID对应的库存量,秒杀成功就将此商品仓库储存量-1。现在只要有1000个线程来秒杀两件货色,500个线程秒杀第三个商品,500个线程秒杀第二个商品。大家来依据这一个轻松的作业场景来解释一下分布式锁。
日常全部秒杀场景的业务系统都相比复杂,承载的业务量非常宏大,并发量也非常高。那样的体系往往采用分布式的结构来平均负载。那么那1000个冒出就能够是从区别的地点过来,商品仓库储存正是分享的财富,也是那1000个并发争抢的能源,那时候大家要求将面世互斥管理起来。那便是遍及式锁的采纳。
而key-value存款和储蓄系统,如redis,因为其部分本性,是促成布满式锁的根本工具。

以上就是本文的全体内容,希望对大家的上学抱有助于,也冀望大家多多关照脚本之家。

那篇作品从作业场景出发,从抽象到落到实处解说了怎么着采纳redis完成布满式锁,达成轻便的秒杀功用,也记录了小编思索的进程,希望能给阅读到本篇文章的人有个别启示。

4503.com,在代码完结规模,表明有现身的秘诀和参数,通过动态代理获取注脚的章程和参数,在代理中加锁,施行完被代理的艺术后获释锁。

lockedObject是参数级的注释,用于声明商品ID等中央项指标参数:

所谓秒杀,从业务角度看,是长期内八个客商“争抢”财富,这里的能源在多数秒杀场景里是货品;将事情抽象,技艺角度看,秒杀正是八个线程对能源开展操作,所以达成秒杀,就非得调整线程对能源的抢夺,既要保障高速并发,也要保管操作的不易。

小结