1.判断A状态是否为0,为0才放行
2.变更A状态为1
3.执行任务
4.填充任务结果
5.复原A状态为0
现在有四个任务,同时有两个线程,会存在两个线程T1和T2都能通过了第一步。此时T2先行,执行通过第5步,将A的状态变更为0。T1继续运行,又将状态变更0,走后续步骤。
问题: 其中的1只能放行一个任务,结果出现了多任务都可以通过,并且任务结果会被后续的T1给覆盖掉了。那么怎么能限制只能放行一个任务呢?
对1、2进行加锁(可以自定义各种,例如synchronize
、Reentranlock
因为此文说明Redis
,所以用了Redis锁
)。
String userId = String.valueOf(user.getUserId());
// Start 判断用户是否有提交任务 (需要上锁,防止并发线程隐患)
redisGateway.lockSettleListStart(userId);
if (Boolean.TRUE.equals(judgeStatus(StartStatusEnum.START_STATUS_STARTED,userId))) {
redisGateway.unlockSettleListStart(userId);
throw new BizException("请取消当前提交任务后再提交");
}
redisGateway.saveStartStatusByUserId(userId, StartStatusEnum.START_STATUS_STARTED.getType());
redisGateway.unlockSettleListStart(userId);
// End
Redis底层的上锁和解锁:
/**
* @description
* @param key: 锁键
* @param id: 随机id
* @param expireTime: 锁过期时间
* @param timeout: 获取锁超时时间
* @return: boolean
*/
public boolean lock(String key, String id, long expireTime, long timeout) {
SetParams params = SetParams.setParams().nx().px(expireTime);
Jedis jedis = jedisPool.getResource();
Long start = System.currentTimeMillis();
try {
for (; ; ) {
//SET命令返回OK ,则证明获取锁成功
String lock = jedis.set(key, id, params);
if ("OK".equals(lock)) {
return true;
}
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if (l >= timeout) {
return false;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} finally {
jedis.close();
}
}
/**
* 解锁
*
* @param id
* @return
*/
public boolean unlock(String key, String id) {
Jedis jedis = jedisPool.getResource();
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
Object result = jedis.eval(script, Collections.singletonList(key),
Collections.singletonList(id));
return "1".equals(result.toString());
} finally {
jedis.close();
}
}
这样1、2步就限制单个线程进入,例如T1进入后修改状态成1,T2再想进去lock的时候redis已经存在该键,一定的时间内无法上锁,则抛异常,不给通过。
除了上锁还能怎么操作呢?实际上我们只要保证查状态和更改状态是一个原子性操作就行了。也就是判断状态和更改状态要么一起运行要么都不运行。这里我们采用今天的主角Redis
的eval
命令之Lua脚本
。
// 阻塞轮询取消
Boolean changeStatusResult = redisGateway.casStartStatusByUser(userId,
StartStatusEnum.START_STATUS_NOT_STARTED.getType(),
StartStatusEnum.START_STATUS_STARTED.getType(),
true);
// 更改状态失败
if (Boolean.FALSE.equals(changeStatusResult)) {
throw new BizException("当前任务暂未完成,请稍后重试");
}
Redis底层CAS原子
/**
* CAS锁,理解成AtomicReference
* @param key
* @param expect
* @param update
* @param expireTime
* @param timeout
* @param condition 同或异
* @return
*/
public Boolean CASLock(String key, String expect, String update, int expireTime, long timeout,Boolean condition) {
Jedis jedis = jedisPool.getResource();
String eval = null;
if (condition) {
eval = "==";
}else {
eval = "!=";
}
Long start = System.currentTimeMillis();
try {
for (; ; ) {
String script =
"if redis.call('get',KEYS[1]) "+eval+" ARGV[1] " +
"or not(redis.call('get', KEYS[1])) then"+
" return redis.call('set',KEYS[2],ARGV[2]) " +
"else" +
" return 0 " +
"end";
Object result = jedis.eval(script, 2,key,key,expect,update);
Boolean end = "OK".equals(result.toString());
if (Boolean.TRUE.equals(end)) {
jedis.expire(key, expireTime);
return true;
}
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if (l >= timeout) {
return false;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} finally {
jedis.close();
}
}
语法:redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:redis.call()
,redis.pcall()
127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) == ARGV[1] or not(redis.call('get',KEYS[1])) then return redis.call('set',KEYS[2],ARGV[2]) else return 0 end" 2 user user whh whh1
OK
127.0.0.1:6379> keys *
1) "LOGIN_TOKEN_KEY:500"
2) "LOGIN_TOKEN_KEY:504"
3) "LOGIN_TOKEN_KEY:501"
4) "LOGIN_TOKEN_KEY:508"
5) "LOGIN_TOKEN_KEY:510"
6) "LOGIN_TOKEN_KEY:505"
7) "LOGIN_TOKEN_KEY:506"
8) "LOGIN_TOKEN_KEY:509"
9) "LOGIN_TOKEN_KEY:507"
10) "LOGIN_TOKEN_KEY:502"
11) "user"
12) "LOGIN_TOKEN_KEY:503"
127.0.0.1:6379> eval "return not(redis.call('get',KEYS[1]))" 1 KEY1 VA
(integer) 1
127.0.0.1:6379> eval "return not(redis.call('get',KEYS[1]))" 1 user VA
(nil)
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) == ARGV[1] or not(redis.call('get',KEYS[1])) then return redis.call('set',KEYS[2],ARGV[2]) else return 0 end" 2 user user whh whh1
(integer) 0
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) == ARGV[1] or not(redis.call('get',KEYS[1])) then return redis.call('set',KEYS[2],ARGV[2]) else return 0 end" 2 user1 user qwe whh1
OK
其中KEYS[1]
和ARGV[1]
对应的就是参数key和arg中的第一个。该句的业务是,如果redis中keys为user值为whh 或者为 null,则更新为whh1 否则返回0。
not(redis.call('get',KEYS[1]))
用于判断是否为null,redis.call('get',KEYS[1])
如果没有该key,则返回 nil(false),not为取反。所以为null则返回(1)ture
>> Redis中LUA判空案例
>> Redis中LUA限流案例
源码Code
1
package redis.clients.jedis;
类:Jedis
@Override
public Object eval(final String script, final int keyCount, final String... params) {
//检查管道和多通道是否开启,jedis不支持,会抛异常。
checkIsInMultiOrPipeline();
client.eval(script, keyCount, params);
client.setTimeoutInfinite();
try {
return getEvalResult();
} finally {
client.rollbackTimeout();
}
}
2
package redis.clients.jedis;
类:BinaryJedis
protected void checkIsInMultiOrPipeline() {
if (client.isInMulti()) {
throw new JedisDataException(
"Cannot use Jedis when in Multi. Please use Transaction or reset jedis state.");
} else if (pipeline != null && pipeline.hasPipelinedResponse()) {
throw new JedisDataException(
"Cannot use Jedis when in Pipeline. Please use Pipeline or reset jedis state .");
}
}
3
package redis.clients.jedis;
类:Client
public void eval(final String script, final int keyCount, final String... params) {
eval(SafeEncoder.encode(script), toByteArray(keyCount), SafeEncoder.encodeMany(params));
}
4
package redis.clients.jedis.util;
类:SafeEncoder 调用这个方法用特定的UTF-8编码转成想要的字符
public static byte[] encode(final String str) {
try {
if (str == null) {
throw new JedisDataException("value sent to redis cannot be null");
}
return str.getBytes(Protocol.CHARSET);
} catch (UnsupportedEncodingException e) {
throw new JedisException(e);
}
}
5
package redis.clients.jedis;
类:BinaryClient
public void eval(final byte[] script, final byte[] keyCount, final byte[][] params) {
sendCommand(EVAL, joinParameters(script, keyCount, params));
}
>>>>>>>joinParameters 就是eval命令中的三个参数转换成对应的二维数组。<<<<<
private byte[][] joinParameters(byte[] first, byte[] second, byte[][] rest) {
byte[][] result = new byte[rest.length + 2][];
result[0] = first;
result[1] = second;
System.arraycopy(rest, 0, result, 2, rest.length);
return result;
}
6
通过connection类
package redis.clients.jedis;
类:Connection
public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {
try {
>>>>>此处jedis开启socket连接(TCP)<<<<<
connect();
>>>>>此处jedis通过输出流发送信息(TCP)<<<<<
Protocol.sendCommand(outputStream, cmd, args);
} catch (JedisConnectionException ex) {
/*
* When client send request which formed by invalid protocol, Redis send back error message
* before close connection. We try to read it to provide reason of failure.
*/
try {
String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
if (errorMessage != null && errorMessage.length() > 0) {
ex = new JedisConnectionException(errorMessage, ex.getCause());
}
} catch (Exception e) {
/*
* Catch any IOException or JedisConnectionException occurred from InputStream#read and just
* ignore. This approach is safe because reading error message is optional and connection
* will eventually be closed.
*/
}
// Any other exceptions related to connection?
broken = true;
throw ex;
}
}
public void connect() {
if (!isConnected()) {
try {
socket = jedisSocketFactory.createSocket();
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException("Failed connecting to "
+ jedisSocketFactory.getDescription(), ex);
}
}
}
7
private static void sendCommand(final RedisOutputStream os, final byte[] command,
final byte[]... args) {
try {
os.write(ASTERISK_BYTE);
os.writeIntCrLf(args.length + 1);
os.write(DOLLAR_BYTE);
os.writeIntCrLf(command.length);
os.write(command);
os.writeCrLf();
for (final byte[] arg : args) {
os.write(DOLLAR_BYTE);
os.writeIntCrLf(arg.length);
os.write(arg);
os.writeCrLf();
}
} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
8
str.getBytes(Protocol.CHARSET) //通过Protocal中指定的编码格式去转换
Protocol类中的数据是一些redis操作的命令和参数的封装类