1. redis加锁分类
- redis能用的的加锁命令分表是
INCR
、SETNX
、SET
2. 第一种锁命令INCR
这种加锁的思路是, key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。
然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。
1、 客户端A请求服务器获取key的值为1表示获取了锁
2、 客户端B也去请求服务器获取key的值为2表示获取锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功
5、 客户端B执行代码完成,删除锁
$redis->incr($key);
$redis->expire($key, $ttl); //设置生成时间为1秒
3. 第二种锁SETNX
这种加锁的思路是,如果 key 不存在,将 key 设置为 value
如果 key 已存在,则 SETNX
不做任何动作
1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
5、 客户端B执行代码完成,删除锁
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
4. 第三种锁SET
上面两种方法都有一个问题,会发现,都需要设置 key 过期。那么为什么要设置key过期呢?如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。于是乎我们需要给锁加一个过期时间以防不测。
但是借助 Expire 来设置就不是原子性操作了。所以还可以通过事务来确保原子性,但是还是有些问题,所以官方就引用了另外一个,使用 SET
命令本身已经从版本 2.6.12 开始包含了设置过期时间的功能。
1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
5、 客户端B执行代码完成,删除锁
$redis->set($key, $value, array('nx', 'ex' => $ttl)); //ex表示秒
5. 其它问题
虽然上面一步已经满足了我们的需求,但是还是要考虑其它问题?
1、 redis发现锁失败了要怎么办?中断请求还是循环请求?
2、 循环请求的话,如果有一个获取了锁,其它的在去获取锁的时候,是不是容易发生抢锁的可能?
3、 锁提前过期后,客户端A还没执行完,然后客户端B获取到了锁,这时候客户端A执行完了,会不会在删锁的时候把B的锁给删掉?
6. 解决办法
针对问题1:使用循环请求,循环请求去获取锁
针对问题2:针对第二个问题,在循环请求获取锁的时候,加入睡眠功能,等待几毫秒在执行循环
针对问题3:在加锁的时候存入的key是随机的。这样的话,每次在删除key的时候判断下存入的key里的value和自己存的是否一样
setnx的Java简单实现:
获取锁:
/**
* 获取一次锁
* @param redisClient
* @return
* <p>true : 获取到锁</p>
* <p>false :未获取到锁</p>
*/
public static boolean getLock(Jedis redisClient) {
//是否获取到锁
boolean hasLock = false;
try {
hasLock = redisClient.setnx(lockKey, "lockObj") == 1;
if (hasLock) {
redisClient.expire(lockKey, lockTime);//一小时
}
} catch (Exception e) {
redisClient.expire(lockKey, lockTime);//一小时
}
return hasLock;
}
此实现会有上面的问题1,只是获取一次,如果获取失败,则不再获取。
如果要继续获取,需要使用方自己实现循环获取逻辑和超时逻辑。
修改下:
/**
* 循环获取锁,直到过超时时间
* @param redisClient
* @param timeout 单位s
* @return
* <p>true : 获取到锁</p>
* <p>false :未获取到锁</p>
*/
public static boolean getLockTimeOut(Jedis redisClient,int timeout) {
//当前的毫秒
long start = System.currentTimeMillis();
//超时时间转换为毫秒单位
timeout = timeout * 1000;
//是否获取到锁
boolean hasLock = false;
while(!hasLock){
try {
hasLock = redisClient.setnx(lockKey, "lockObj") == 1;
//获取到锁
if (hasLock) {
redisClient.expire(lockKey, lockTime);//一小时
} else {
//未获取到锁,判断是否超过超时时间
long now = System.currentTimeMillis();
//当前时间超过起始时间的时间间隔,当间隔>=超时时间,则停止获取锁
if(now - start >= timeout){
System.out.println("--------获取锁超时,不再获取--------");
break;
}
//睡眠,降低抢锁频率,缓解redis压力
Thread.sleep(500);
}
} catch (Exception e) {
redisClient.expire(lockKey, lockTime);//一小时
}
}
return hasLock;
}
此种实现,通过Thread.sleep(500)来降低抢锁频率,用以处理问题2,同时,通过while来实现循环获取锁逻辑,直到超过超时时间。这样一来,外部调用时就不用再考虑自实现循环和超时问题了。
针对问题3,还未实现,晚点实现再发吧
附一个简单测试类:
package com.paic.elis.elis_smp_cms.redis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisCount2 {
static JedisPoolConfig config = new JedisPoolConfig();
static JedisPool newPool = null;
static Jedis jedisCli = null;
static int lockTime = 60 * 60; //单位:S
static final String lockKey = "lockKey";
static {
config.setMaxTotal(20);
newPool = new JedisPool(config, "10.20.130.34", 4436,20000,"quul5trl");
jedisCli = newPool.getResource();
jedisCli.del("mqCount");
jedisCli.del(lockKey);
}
public static void main(String[] args) {
// test1();
// test2();
test3();
// test1();
}
//多个线程去获取锁,仅有获取到锁的线程才会执行,其他线程被丢弃
public static void test1(){
for(int i = 0 ; i < 10; i++){
new Thread(){
public void run(){
Jedis jedisC = newPool.getResource();
boolean flag = false;
flag = getLock(jedisC);
//获取到锁
if(flag){
System.out.println("获取到锁,开始处理");
//业务逻辑执行
for(int i = 0 ; i < 100 ; i++){
jedisC.incr("mqCount");
}
System.out.println(jedisC.get("mqCount"));
releaseLock(jedisC);
System.out.println("释放锁成功");
}
}
}.start();
}
}
//多个线程去获取锁,按照获取到锁的顺序执行,一直等到所有线程执行完毕
public static void test2(){
for(int i = 0 ; i < 10; i++){
new Thread(){
public void run(){
Jedis jedisC = newPool.getResource();
boolean flag = false;
while(!flag){
flag = getLock(jedisC);
//获取到锁
if(flag){
System.out.println("获取到锁,开始处理");
//业务逻辑执行
for(int i = 0 ; i < 100 ; i++){
jedisC.incr("mqCount");
}
System.out.println(jedisC.get("mqCount"));
releaseLock(jedisC);
System.out.println("释放锁成功");
}
}
}
}.start();
}
}
//多个线程去获取锁,按照获取到锁的顺序执行,等到超时时间之后仍未执行的线程被丢弃
public static void test3(){
for(int i = 0 ; i < 10; i++){
new Thread(){
public void run(){
Jedis jedisC = newPool.getResource();
boolean flag = false;
flag = getLockTimeOut(jedisC,2);
//获取到锁
if(flag){
System.out.println("获取到锁,开始处理");
//业务逻辑执行
for(int i = 0 ; i < 100 ; i++){
jedisC.incr("mqCount");
}
System.out.println(jedisC.get("mqCount"));
releaseLock(jedisC);
System.out.println("释放锁成功");
}
}
}.start();
}
}
/**
* 获取到锁则执行,未获取则不执行(放弃本次执行)
* @param redisClient
* @return
*/
public static void doMethod(Jedis redisClient) {
//未获取到锁--直接返回
if(!getLock(redisClient)) {
return;
}
//获取到锁,开始处理
try{
System.out.println("获取到锁,开始处理");
//业务逻辑执行
for(int i = 0 ; i < 100 ; i++){
redisClient.incr("mqCount");
}
System.out.println(redisClient.get("mqCount"));
} finally {
// 只要获取到锁,则在业务逻辑结束之后,必须释放锁
releaseLock(redisClient);
}
}
/**
* 获取到锁则执行,未获取则一直尝试获取,直到获取到锁为止
* @param redisClient
* @return
*/
public static void doMethodContinue(Jedis redisClient) {
boolean flag = false;
while(!flag){
flag = getLock(redisClient);
//如果获取到锁,则继续执行,否则循环获取
if(flag){
try{
System.out.println("获取到锁,开始处理");
} finally {
// 只要获取到锁,则在业务逻辑结束之后,必须释放锁
releaseLock(redisClient);
}
}
//间隔0.5s再次获取
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
/**
* 获取到锁则执行,未获取则一直尝试获取,直到到达超时时间
* @param redisClient
* @param timeout 单位S
*/
public static void doMethodContinueTimeout(Jedis redisClient,int timeout) {
//当前的毫秒
long start = System.currentTimeMillis();
//超时时间转换为毫秒单位
timeout = timeout * 1000;
boolean flag = false;
while(!flag){
flag = getLock(redisClient);
//如果获取到锁,则继续执行,否则循环获取
if(flag){
try{
System.out.println("获取到锁,开始处理");
} finally {
// 只要获取到锁,则在业务逻辑结束之后,必须释放锁
releaseLock(redisClient);
}
}
long now = System.currentTimeMillis();
//当前时间超过起始时间的时间间隔,当间隔>=超时时间,则停止获取锁
if(now - start >= timeout){
flag = true;
}
//睡眠,降低抢锁频率,缓解redis压力
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 获取一次锁
* @param redisClient
* @return
* <p>true : 获取到锁</p>
* <p>false :未获取到锁</p>
*/
public static boolean getLock(Jedis redisClient) {
//是否获取到锁
boolean hasLock = false;
try {
hasLock = redisClient.setnx(lockKey, "lockObj") == 1;
if (hasLock) {
redisClient.expire(lockKey, lockTime);//一小时
}
} catch (Exception e) {
redisClient.expire(lockKey, lockTime);//一小时
}
return hasLock;
}
/**
* 循环获取锁,直到过超时时间
* @param redisClient
* @param timeout 单位s
* @return
* <p>true : 获取到锁</p>
* <p>false :未获取到锁</p>
*/
public static boolean getLockTimeOut(Jedis redisClient,int timeout) {
//当前的毫秒
long start = System.currentTimeMillis();
//超时时间转换为毫秒单位
timeout = timeout * 1000;
//是否获取到锁
boolean hasLock = false;
while(!hasLock){
try {
hasLock = redisClient.setnx(lockKey, "lockObj") == 1;
//获取到锁
if (hasLock) {
redisClient.expire(lockKey, lockTime);//一小时
} else {
//未获取到锁,判断是否超过超时时间
long now = System.currentTimeMillis();
//当前时间超过起始时间的时间间隔,当间隔>=超时时间,则停止获取锁
if(now - start >= timeout){
System.out.println("--------获取锁超时,不再获取--------");
break;
}
//睡眠,降低抢锁频率,缓解redis压力
Thread.sleep(500);
}
} catch (Exception e) {
redisClient.expire(lockKey, lockTime);//一小时
}
}
return hasLock;
}
/**
* 释放锁
* @param redisClient
*/
public static void releaseLock(Jedis redisClient) {
redisClient.del(lockKey);
}
}
主要转自:
http://blog.csdn.net/Dennis_ukagaka/article/details/78072274