分布式锁之redis实现

当我并发测试时:

$ ab -n 500 -c 100 http://localhost:8080/seckill/1/

Java之道|使用Redis分布式锁处理并发,解决超卖问题

 

这TM肯定不行啊,这就超卖了,明明没这么多商品,结果还卖出去了。。。

二、synchronized处理并发

首先,synchronized的确是一个解决办法,而且也很简单,在方法前面加一个synchronized关键字。

但是通过压测,发现请求变的很慢,因为:

synchronized就用一个锁把这个方法锁住了,每次访问这个方法,只会有一个线程,所以这就是它导致慢的原因。通过这种方式,保证这个方法中的代码都是单线程来处理,不会出什么问题。

同时,使用synchronized还是存在一些问题的,首先,它无法做到细粒度的控制,比如同一时间有秒杀A商品和B商品的请求,都进入到了这个方法,虽然秒杀A商品的人很多,但是秒杀B商品的人很少,但是即使是买B商品,进入到了这个方法,也会一样的慢。

最重要的是,它只适合单点的情况。如果以后程序水平扩展了,弄了个集群,很显然,负载均衡之后,不同的用户看到的结果一定是五花八门的。

所以,还是使用更好的办法,使用redis分布式锁。

三、redis分布式锁

1、两个redis的命令

  • SETNX key value
  • GETSET key value

setnx key value 简单来说,setnx就是,如果没有这个key,那么就set一个key-value, 但是如果这个key已经存在,那么将不会再次设置,get出来的value还是最开始set进去的那个value.

网站中还专门讲到可以使用!SETNX加锁,如果获得锁,返回1,如果返回0,那么该键已经被其他的客户端锁定。

并且也提到了如何处理死锁。

getset key value 这个就更简单了,先通过key获取value,然后再将新的value set进去。

2、redis分布式锁的实现

我们希望的,无非就是这一段代码,能够单线程的去访问,因此在这段代码之前给他加锁,相应的,这段代码后面要给它解锁:

Java之道|使用Redis分布式锁处理并发,解决超卖问题

 

2.1 引入redis依赖

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.2 配置redis

spring:
 redis:
 host: localhost
 port: 6379

2.3 编写加锁和解锁的方法

package com.vito.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
 * Created by VitoYi on 2018/4/5.
 */
@Component
public class RedisLock {
 Logger logger = LoggerFactory.getLogger(this.getClass());
 @Autowired
 private StringRedisTemplate redisTemplate;
 /**
 * 加锁
 * @param key 商品id
 * @param value 当前时间+超时时间
 * @return
 */
 public boolean lock(String key, String value) {
 if (redisTemplate.opsForValue().setIfAbsent(key, value)) { //这个其实就是setnx命令,只不过在java这边稍有变化,返回的是boolea
 return true;
 }
 //避免死锁,且只让一个线程拿到锁
 String currentValue = redisTemplate.opsForValue().get(key);
 //如果锁过期了
 if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
 //获取上一个锁的时间
 String oldValues = redisTemplate.opsForValue().getAndSet(key, value);
 /*
 只会让一个线程拿到锁
 如果旧的value和currentValue相等,只会有一个线程达成条件,因为第二个线程拿到的oldValue已经和currentValue不一样了
 */
 if (!StringUtils.isEmpty(oldValues) && oldValues.equals(currentValue)) {
 return true;
 }
 }
 return false;
 }
 /**
 * 解锁
 * @param key
 * @param value
 */
 public void unlock(String key, String value) {
 try {
 String currentValue = redisTemplate.opsForValue().get(key);
 if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
 redisTemplate.opsForValue().getOperations().delete(key);
 }
 } catch (Exception e) {
 logger.error("『redis分布式锁』解锁异常,{}", e);
 }
 }
}

为什么要有避免死锁的一步呢?

假设没有『避免死锁』这一步,结果在执行到下单代码的时候出了问题,毕竟操作数据库、网络、io的时候抛了个异常,这个异常是偶然抛出来的,就那么偶尔一次,那么会导致解锁步骤不去执行,这时候就没有解锁,后面的请求进来自然也或得不到锁,这就被称之为死锁。

而这里的『避免死锁』,就是给锁加了一个过期时间,如果锁超时了,就返回true,解开之前的那个死锁。

2.4 下单代码中引入加锁和解锁,确保只有一个线程操作

@Autowired
private RedisLock redisLock;
@Override
@Transactional
public String seckill(Integer id)throws RuntimeException {
 //加锁
 long time = System.currentTimeMillis() + 1000*10; //超时时间:10秒,最好设为常量
 boolean isLock = redisLock.lock(String.valueOf(id), String.valueOf(time));
 if(!isLock){
 throw new RuntimeException("人太多了,换个姿势再试试~");
 }
 //查库存
 Product product = productMapper.findById(id);
 if(product.getStock()==0) throw new RuntimeException("已经卖光");
 //写入订单表
 Order order=new Order();
 order.setProductId(product.getId());
 order.setProductName(product.getName());
 orderMapper.add(order);
 //减库存
 product.setPrice(null);
 product.setName(null);
 product.setStock(product.getStock()-1);
 productMapper.update(product);
 //解锁
 redisLock.unlock(String.valueOf(id),String.valueOf(time));
 return findProductInfo(id);
}

这样再来跑几次压测,就不会超卖了:

Java之道|使用Redis分布式锁处理并发,解决超卖问题

 

封装-----

/**

* @Title: checkSoldCountByRedisDate

* @Description: 抢购的计数处理(用于处理超卖)

* @param @param key 购买计数的key

* @param @param limitCount 总的限购数量

* @param @param buyCount 当前购买数量

* @param @param endDate 抢购结束时间

* @param @param lock 锁的名称与unDieLock方法的lock相同

* @param @param expire 锁占有的时长(毫秒)

* @param @return 设定文件

* @return boolean 返回类型

* @throws

*/

private boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate, String lock, int expire) {

boolean check = false;

if (this.lock(lock, expire)) {

Integer soldCount = (Integer) redisUtil.get(key);

Integer totalSoldCount = (soldCount == null ? 0 : soldCount) + buyCount;

if (totalSoldCount <= limitCount) {

redisUtil.set(key, totalSoldCount, DateUtil.diffDateTime(endDate, new Date()));

check = true;

}

redisUtil.remove(lock);

} else {

if (this.unDieLock(lock)) {

logger.info("解决了出现的死锁");

} else {

throw new ServiceException("活动太火爆啦,请稍后重试");

}

}

return check;

}

/**

* @Title: lock

* @Description: 加锁机制

* @param @param lock 锁的名称

* @param @param expire 锁占有的时长(毫秒)

* @param @return 设定文件

* @return Boolean 返回类型

* @throws

*/

@SuppressWarnings("unchecked")

public Boolean lock(final String lock, final int expire) {

return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {

@Override

public Boolean doInRedis(RedisConnection connection) throws DataAccessException {

boolean locked = false;

byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtil.getDateAddMillSecond(null, expire));

byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);

locked = connection.setNX(lockName, lockValue);

if (locked)

connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.MILLISECONDS));

return locked;

}

});

}

/**

* @Title: unDieLock

* @Description: 处理发生的死锁

* @param @param lock 是锁的名称

* @param @return 设定文件

* @return Boolean 返回类型

* @throws

*/

@SuppressWarnings("unchecked")

public Boolean unDieLock(final String lock) {

boolean unLock = false;

Date lockValue = (Date) redisTemplate.opsForValue().get(lock);

if (lockValue != null && lockValue.getTime() <= (new Date().getTime())) {

redisTemplate.delete(lock);

unLock = true;

}

return unLock;

}

下面会把上面方法中用到的相关DateUtil类的方法贴出来:

/**

* 日期相减(返回秒值)

* @param date Date

* @param date1 Date

* @return int

* @author

*/

public static Long diffDateTime(Date date, Date date1) {

return (Long) ((getMillis(date) - getMillis(date1))/1000);

}

public static long getMillis(Date date) {

Calendar c = Calendar.getInstance();

c.setTime(date);

return c.getTimeInMillis();

}

/**

* 获取 指定日期 后 指定毫秒后的 Date

* @param date

* @param millSecond

* @return

*/

public static Date getDateAddMillSecond(Date date, int millSecond) {

Calendar cal = Calendar.getInstance();

if (null != date) {// 没有 就取当前时间

cal.setTime(date);

}

cal.add(Calendar.MILLISECOND, millSecond);

return cal.getTime();

}

新补充:

import java.util.Calendar;

import java.util.Date;

import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

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.core.TimeoutUtils;

import org.springframework.stereotype.Component;

import cn.mindmedia.jeemind.framework.utils.redis.RedisUtils;

import cn.mindmedia.jeemind.utils.DateUtils;

/**

* @ClassName: LockRetry

* @Description: 此功能只用于促销组

* @date 2017年7月29日 上午11:54:54

*/

@SuppressWarnings("rawtypes")

@Component("lockRetry")

public class LockRetry {

private Logger logger = LoggerFactory.getLogger(getClass());

@Autowired

private RedisTemplate redisTemplate;

/**

*

* @Title: retry

* @Description: 重入锁

* @param @param lock 名称

* @param @param expire 锁定时长(秒),建议10秒内

* @param @param num 取锁重试试数,建议不大于3

* @param @param interval 重试时长

* @param @param forceLock 强制取锁,不建议;

* @param @return

* @param @throws Exception 设定文件

* @return Boolean 返回类型

* @throws

*/

@SuppressWarnings("unchecked")

public Boolean retryLock(final String lock, final int expire, final int num, final long interval, final boolean forceLock) throws Exception {

Date lockValue = (Date) redisTemplate.opsForValue().get(lock);

if (forceLock) {

RedisUtils.remove(lock);

}

if (num <= 0) {

if (null != lockValue && lockValue.getTime() >= (new Date().getTime())) {

logger.debug(String.valueOf((lockValue.getTime() - new Date().getTime())));

Thread.sleep(lockValue.getTime() - new Date().getTime());

RedisUtils.remove(lock);

return retryLock(lock, expire, 1, interval, forceLock);

}

return false;

} else {

return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {

@Override

public Boolean doInRedis(RedisConnection connection) throws DataAccessException {

boolean locked = false;

byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtils.getDateAdd(null, expire, Calendar.SECOND));

byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);

logger.debug(lockValue.toString());

locked = connection.setNX(lockName, lockValue);

if (locked)

return connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.SECONDS));

else {

try {

Thread.sleep(interval);

return retryLock(lock, expire, num - 1, interval, forceLock);

} catch (Exception e) {

e.printStackTrace();

return locked;

}

}

}

});

}

}

}

/**

*

* @Title: getDateAddMillSecond

* @Description: (TODO)取将来时间

* @param @param date

* @param @param millSecond

* @param @return 设定文件

* @return Date 返回类型

* @throws

*/

public static Date getDateAdd(Date date, int expire, int idate) {

Calendar calendar = Calendar.getInstance();

if (null != date) {// 默认当前时间

calendar.setTime(date);

}

calendar.add(idate, expire);

return calendar.getTime();

}

/**

* 删除对应的value

* @param key

*/

public static void remove(final String key) {

if (exists(key)) {

redisTemplate.delete(key);

}

}

/**

* 判断缓存中是否有对应的value

* @param key

* @return

*/

public static boolean exists(final String key) {

return stringRedisTemplate.hasKey(key);

}

private static StringRedisTemplate stringRedisTemplate = ((StringRedisTemplate) SpringContextHolder.getBean("stringRedisTemplate"));

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值