今天聊聊redis分布式锁(redis单机版本),锁需要满足一下几点:
a 互斥行:同一时刻只能有一个线程获取锁,只有当该线程执行完业务逻辑释放锁以后,其他线程才能尝试获取锁。
b 保证锁的释放,当A服务器加锁成功后宕机,不能影响其他服务器获取锁,这个可以通过过期时间来设置
c A线程加锁,这个锁只能由A线程去解锁,其他线程不能解锁A线程加的锁,否则就乱套了(如果被其他线程解锁,那么其他线程可能成功获取锁,这样就不是串行执行了)。
下面给出实现代码:
1 引人依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.29</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2 application.yml配置文件:
server: port: 80spring: datasource: username: rootpassword: 123456url: jdbc:mysql://192.168.137.129:3306/my_dbtest?useUnicode=true&characterEncoding=utf-8driver-class-name: com.mysql.jdbc.Driverredis: database: 0host: 127.0.0.1port: 6379password: jedis: pool: max-active: 50max-wait: -1max-idle: 50min-idle: 1timeout: 10000
3 JedisPool 连接池配置类:
@Configuration
@PropertySource("classpath:application.yml")public class RedisConfig {
@Value("${spring.redis.host}")private String host;
@Value("${spring.redis.port}")private int port;
@Value("${spring.redis.timeout}")private int timeout;
@Value("${spring.redis.jedis.pool.max-active}")private int maxActive;
@Value("${spring.redis.jedis.pool.max-idle}")private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")private int minIdle;
@Value("${spring.redis.jedis.pool.max-wait}")private long maxWaitMillis;
@Beanpublic JedisPool redisPoolFactory() throws Exception {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxIdle(minIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);return jedisPool;
}
}
4 RedisLock类:
@Componentpublic class RedisLock {private final String LOCK_MSG = "OK";private final Long UNLOCK_MSG = 1L;private final String SET_IF_NOT_EXIST = "NX";private final String SET_WITH_EXPIRE_TIME = "PX";/** * 获取锁 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */public boolean Lock(Jedis jedis,String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_MSG.equals(result)) {return true;
}return false;
}/** * 阻塞获取锁 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime lockKey超时时间 * @param millisTimeout 超时获取锁时间 * @return 是否获取成功 */public boolean tryLockMillis(Jedis jedis,String lockKey, String requestId, int expireTime,int millisTimeout ) {
String result=null;long now= System.currentTimeMillis();while (System.currentTimeMillis()-now<millisTimeout){
result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);if (LOCK_MSG.equals(result)) {return true;
}
}return false;
}/** * 释放锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */public boolean unLock(Jedis jedis,String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));if (UNLOCK_MSG.equals(result)) {return true;
}return false;
}
}
5 测试类
@Slf4j
@RestControllerpublic class TestController {
@Autowiredprivate OrderService orderService;private final ExecutorService executorService = Executors.newFixedThreadPool(50);
@Autowiredprivate RedisLock redisLock;
@Autowiredprivate JedisPool jedisPool;private int num;
@RequestMapping("/testRedisLock")public String testRedisLock() throws Exception {
String lockKey="idgenerate";
String requestId = UUID.randomUUID().toString();
Jedis jedis = jedisPool.getResource();try {boolean lock = redisLock.tryLockMillis(jedis,lockKey,requestId, 1000*60,1000*1000);if(lock){
Order order = new Order();
order.setId(null);
order.setOrderId(String.valueOf(++num));
order.setOrderTime(new Date());executorService.execute(new Runnable() {
@Overridepublic void run() {orderService.insert(order);
}
});
System.out.println(num);
}
}catch (Exception e){log.info("",e);
}finally {redisLock.unLock(jedis, lockKey, requestId);
jedis.close();
}return "SUCCESS";
}
}
用jmeter测试
开始时间:2019-12-12 16:01:12
结束时间:2019-12-12 16:04:53
总共耗时3分41秒大约2毫秒保存完成一笔订单,redis性能牛逼啊。。。
下面关系加锁解锁代码分析一下
tryLockMillis超时获取锁:
lockKey: jedis.set方法参数的key
requestId: jedis.set方法参数的value 必须保证唯一,这里测试是用的uuid保证唯一性,解锁的时候需要根据这个参数去解锁,保证当前获取锁的线下去解锁。
SET_IF_NOT_EXIST:这个参数保证只有一个线程能set成功返回ok
SET_WITH_EXPIRE_TIME:表示要设置超时时间
expireTime:key超时时间(毫秒),这个具体设置多久跟你的业务逻辑执行的时长有关
millisTimeout:阻塞获取锁的时间,这个具体设置多久跟你的并发量和业务逻辑执行的时长相关
jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)这个方法是原子操作(redis保证),成功set返回ok,同一时刻只能有一个线程可以设置成功返回ok,其他线下在设置返回的不是ok
不能把这个命令拆开,先设置key然后在设置超时时间,这样不是原子操作,解锁也存在类似问题,Lock方法为非阻塞获取锁。
public boolean unLock(Jedis jedis,String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));if (UNLOCK_MSG.equals(result)) {return true;
}return false;
}
script lua脚本的意思是:根据传入的lockKey作为KEYS[1]查找value ,如果找到的value和传入的requestId(作为ARGV[1]参数)相等就返回1
完结,觉得不错,关注一下,不足不处,望指点,互相学习进步!