1,基本思路是:基于Redis实现分布式锁+幂等性;
2,具体实现逻辑:
使用redis的decr (对key对应的数字做减1操作。如果key不存在,那么在操作之前,这个key对应的值会被置为0。如果key有一个错误类型的value或者是一个不能表示成数字的字符串,就返回错误。这个操作最大支持在64位有符号的整型数字。)可以实现原子性的递增递减操作控制优惠码不超送,然后给每个用户维护一个userid+优惠码活动的key保证幂等性,只要redis存在这种key,那就代表已经领取了,具体的优惠码分发可以异步执行。为了避免竞争(同一个用户,多个设备同时领取)使用Redis.setNX() 实现分布式锁(重复数据插入可用其来实现排他锁);
3,相关核心代码逻辑
ApplicationContext context = new AnnotationConfigApplicationContext(RedisCacheConfiguration.class); JedisPool jedisPool = (JedisPool)context.getBean("redisPoolFactory"); /** * 优惠劵发放主逻辑 * @param user 用户数据 * @param eventsPromoCodeMark 本次活动唯一标记 */ public void redisAcquireLockLock(User user, String eventsPromoCodeMark){ //活动上线前设置优惠码总量一百万:jedisResources.set(promoCodeNumberKey, "1000000"); Jedis jedisResources = jedisPool.getResource(); String lock = user.getId() + eventsPromoCodeMark;//设置本次活动优惠码锁(保证幂等性):userId + 优惠码活动的key String promoCodeNumberKey = eventsPromoCodeMark + "promoCodeNumber"; //设置优惠码总量key String userKey = lock + "receiveCoupon";//设置用户优惠码领取key(自定义,只要保证一个用户在一个活动的唯一性即可) //优惠码领取库存校验 boolean promoCode = jedisResources.exists(promoCodeNumberKey) && StringUtils.isNotBlank(jedisResources.get(promoCodeNumberKey)) && 0L < Long.valueOf(jedisResources.get(promoCodeNumberKey)); if(!promoCode){ System.out.println("优惠码已经领取完了"); jedisResources.close(); return; } //为了避免竞争(同一个用户,多个设备同时领取).使用Redis.setNX() 实现分布式锁(重复数据插入可用其来实现排他锁) boolean flag = acquireLock(lock); if(flag){ //1,获取锁成功,进行用户已领取标记,查询用户是否已经领取过 Boolean isExists = jedisResources.exists(userKey); if(!isExists){ //2,先使用redis的decr可以实现原子性的递增递减操作控制优惠码不超送, Long success = jedisResources.decr(promoCodeNumberKey); // 先减库存后发码(减库存后返回的现有库存数量大于等于0说明本次抢码成功,再进行发送优惠码,否则库存已经空了就不进行发送优惠码) if(success >= 0L){ //3,再进行优惠码分发(可异步执行通过MQ进行发放,如果减库存成功,发放失败,进行发放补偿性操作) jedisResources.set(userKey, "received"); System.out.println("领取成功"); long oldValue = Long.valueOf(jedisResources.get(lock));//获取锁的旧值 if (oldValue > System.currentTimeMillis()) {//检查处理时间是否小于超时时间,释放锁 jedisResources.delete(lock); } } } else { System.out.println("已经领取过了"); } } else { System.out.println("请重试"); } jedisResources.close(); } /** * 获取锁方法 * @param lock 锁的k * @return boolean 本次获锁是否成功 */ public boolean acquireLock(String lock) { boolean success;//设置获锁成功标记 Jedis jedis = jedisPool.getResource(); int expired = 60 * 1000;//强制过期时间:60 秒 long value = System.currentTimeMillis() + expired; long acquired = jedis.setnx(lock, String.valueOf(value)); //通过SETNX试图获取一个lock if (1 == acquired) {//setnx 返回值为1则表示设置成功,则成功获取一个锁 success = true; } else {//setnx 失败,说明锁仍然被其他对象保持,检查其是否已经超时 long oldValue = Long.valueOf(jedis.get(lock));//获取锁的旧值 if (oldValue < System.currentTimeMillis()) {//检查是否超时 String getOldValue = jedis.getSet(lock, String.valueOf(value));//超时,进行锁覆盖,并获取覆盖值之后的获取到的旧值 // 进行最终获取锁是否成功原子性校验(覆盖旧值之前获取到的旧值 和 覆盖值之后的获取到的旧值 相同 则本次获锁成功,否则 失败已经被别的对象获取到) if (Long.valueOf(getOldValue) == oldValue) { success = true; } else {// 已被其他进程捷足先登了 success = false; } } else {//未超时,则直接返回获取锁失败 success = false; } } jedisPool.close(); return success; }
获锁具体的使用步骤如下:
1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。