单机下:
只适用于单机环境下(单个JVM),多个客户端访问同一个服务器
1.synchronized
package com.cloud.SR.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class TestConrtoller1 { @Value("${server.port}") private String serverPort; @Resource private StringRedisTemplate stringRedisTemplate; @GetMapping("/buy1") public String shopping(){ synchronized (this){ String result = stringRedisTemplate.opsForValue().get("goods:001"); int total = result == null? 0 :Integer.parseInt(s); if(total > 0){ int realTotal = total - 1; stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realCount )); System.out.println("剩余商品为:"+realCount +",提供服务的端口号:"+serverPort); return "剩余商品为:"+realTotal +",提供服务的端口号:"+serverPort; }else{ System.out.println("购买商品失败!"); } return "购买商品失败!"; } } }
2.ReentrantLock
@RestController public class TestConrtoller2 { @Value("${server.port}") private String serverPort; // 使用ReentrantLock锁解决单体应用的并发问题 Lock lock = new ReentrantLock(); @Autowired StringRedisTemplate stringRedisTemplate; @RequestMapping("/buy2") public String index() { lock.lock(); try { String result = stringRedisTemplate.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:" + realTotal + ",服务端口为:"+serverPort); return "购买商品成功,库存还剩:" + realTotal + ",服务端口为:"+serverPort; } else { System.out.println("购买商品失败!"); } } catch (Exception e) { lock.unlock(); } finally { lock.unlock(); } return "购买商品失败!"; } }
分布式下:
而在服务器分布式集群下,,单个服务器的synchronized和ReentrantLock
1.SETNX
SET key value [EX seconds] [PX milliseconds] [NX|XX]
-
EX
seconds – 设置键key的过期时间,单位时秒 -
PX
milliseconds – 设置键key的过期时间,单位时毫秒 -
NX
– 只有键key不存在的时候才会设置key的值 -
XX
– 只有键key存在的时候才会设置key的值
例子:
1.set lock01 01 NX :意思就是说只要谁把key为lock01的值设置为01且key不存在的时候就能拿到锁
2. set lock01 01 NX EX 30 :在例1的基础上把锁设置的时间设置为30秒后过期。避免有服务挂了而没有释放锁的情况、或者业务处理完但一直拿着锁不释放导致死锁。
项目中使用SETNX:
template.opsForValue().setIfAbsent()
测试的话就得本机模拟集群,当然有虚拟机的也可以用两台虚拟机,但此处用两台JVM即可完成简易集群本机实现集群的可以看这篇文章:http://t.csdn.cn/jvZFx
先让集群跑起来,然后启动Nginx,再通过Jmeter实现高并发的秒杀环节
用template.opsForValue().setIfAbsent()命令进行加锁。加上了过期时间后就解决了key无法删除的问题,但如果key设置的时间太短,当业务处理的时间长于key设置的时间,key过期后其他请求就可以设置这个key而当这个线程再回来处理这个程序的时候就会把人家设置的key给删除了,因此我们规定谁设置的锁只能由谁删除。
finally { // 谁加的锁,谁才能删除 if(template.opsForValue().get(REDIS_LOCK).equals(value)){ template.delete(REDIS_LOCK); }
而新的问题就是finally块
的判断和del
删除操作不是原子操作,并发的时候也会出问题。因此采用lua(原子性)来进行删除
finally { // 谁加的锁,谁才能删除,使用Lua脚本,进行锁的删除 Jedis jedis = null; try{ jedis = RedisUtils.getJedis(); String script = "if redis.call('get',KEYS[1]) == ARGV[1] " + "then " + "return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value)); if("1".equals(eval.toString())){ System.out.println("-----del redis lock ok...."); }else{ System.out.println("-----del redis lock error ...."); } }catch (Exception e){ }finally { if(null != jedis){ jedis.close(); } }
总的代码:
@RestController public class TestConrtoller3 { @Value("${server.port}") private String serverPort; public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate stringtemplate; @RequestMapping("/buy3") public String shopping(){ // 每个人进来先要进行加锁,key值为"good_lock",且用UUID保证每个人的锁不同 String value = UUID.randomUUID().toString().replace("-",""); try{ // 为key加一个过期时间 Boolean flag = stringtemplate.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS); // 加锁失败 if(!flag){ return "抢锁失败!"; } System.out.println( value+ " 抢锁成功"); String result = stringtemplate.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此处需要调用其他微服务,处理时间较长。。。 int realTotal = total - 1; stringtemplate.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:" + realTotal + ",服务端口为"+serverPort); return "购买商品成功,库存还剩:" + realTotal + "服务端口为"+serverPort; } else { System.out.println("购买商品失败"); } return "购买商品失败!"; }finally { // 谁加的锁,谁才能删除,使用Lua脚本,进行锁的删除 Jedis jedis = null; try{ jedis = RedisUtils.getJedis(); String script = "if redis.call('get',KEYS[1]) == ARGV[1] " + "then " + "return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value)); if("1".equals(eval.toString())){ System.out.println("-----del redis lock ok...."); }else{ System.out.println("-----del redis lock error ...."); } }catch (Exception e){ }finally { if(null != jedis){ jedis.close(); } } } } }
2.Redisson(推荐)
考虑缓存续命,以及Redis
集群部署下,异步复制造成的锁丢失:主节点没来得及把刚刚set
进来这条数据给从节点,就挂了。所以直接上RedLock
的Redisson
落地实现
@RestController public class TestConrtoller4 { @Value("${server.port}") private String serverPort; public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate stringtemplate; @Autowired Redisson redisson; @RequestMapping("/buy4") public String shopping(){ RLock lock = redisson.getLock(REDIS_LOCK); lock.lock(); // 每个人进来先要进行加锁,key值为"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ String result = stringtemplate.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此处需要调用其他微服务,处理时间较长 int realTotal = total - 1; stringtemplate.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为"+serverPort); return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为"+serverPort; } else { System.out.println("购买商品失败"); } return "购买商品失败"; }finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } }
Redis工具类
import com.myfutech.common.util.constant.RedisPrefix; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.types.Expiration; import java.nio.charset.StandardCharsets; import java.util.UUID; /** * 基于redis分布式锁 */ @Slf4j public class RedisLockUtils { /** * 默认轮休获取锁间隔时间, 单位:毫秒 */ private static final int DEFAULT_ACQUIRE_RESOLUTION_MILLIS = 100; private static final String UNLOCK_LUA; static { StringBuilder sb = new StringBuilder(); sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] "); sb.append("then "); sb.append(" return redis.call(\"del\",KEYS[1]) "); sb.append("else "); sb.append(" return 0 "); sb.append("end "); UNLOCK_LUA = sb.toString(); } /** * 获取锁,没有获取到则一直等待,异常情况则返回null * * @param redisTemplate redis连接 * @param key redis key * @param expire 锁过期时间, 单位 秒 * @return 当前锁唯一id,如果没有获取到,返回 null */ public static String lock(RedisTemplate redisTemplate, final String key, long expire){ return lock(redisTemplate, key, expire, -1); } /** * 获取锁,acquireTimeout时间内没有获取到,则返回null,异常情况返回null * * @param redisTemplate redis连接 * @param key redis key * @param expire 锁过期时间, 单位 秒 * @param acquireTimeout 获取锁超时时间, -1代表永不超时, 单位 秒 * @return 当前锁唯一id,如果没有获取到,返回 null */ public static String lock(RedisTemplate redisTemplate, final String key, long expire, long acquireTimeout){ try { return acquireLock(redisTemplate, key, expire, acquireTimeout); } catch (Exception e) { log.error("acquire lock exception", e); } return null; } /** * 获取锁,没有获取到则一直等待,没有获取到则抛出异常 * * @param redisTemplate redis连接 * @param key redis key * @param expire 锁过期时间, 单位 秒 * @return 当前锁唯一id,如果没有获取到,返回 null */ public static String lockFailThrowException(RedisTemplate redisTemplate, final String key, long expire){ return lockFailThrowException(redisTemplate, key, expire, -1); } /** * 获取锁,到达超时时间时没有获取到,则抛出异常 * * @param redisTemplate redis连接 * @param key redis key * @param expire 锁过期时间, 单位 秒 * @param acquireTimeout 获取锁超时时间, -1代表永不超时, 单位 秒 * @return 当前锁唯一id,如果没有获取到,返回 null */ public static String lockFailThrowException(RedisTemplate redisTemplate, final String key, long expire, long acquireTimeout){ try { String lockId = acquireLock(redisTemplate, key, expire, acquireTimeout); if (lockId != null) { return lockId; } throw new RuntimeException("acquire lock fail"); } catch (Exception e) { throw new RuntimeException("acquire lock exception", e); } } private static String acquireLock(RedisTemplate redisTemplate, String key, long expire, long acquireTimeout) throws InterruptedException { long acquireTime = -1; if (acquireTimeout != -1) { acquireTime = acquireTimeout * 1000 + System.currentTimeMillis(); } synchronized (key) { String lockId = UUID.randomUUID().toString(); while (true) { if (acquireTime != -1 && acquireTime < System.currentTimeMillis()) { break; } //调用tryLock boolean hasLock = tryLock(redisTemplate, key, expire, lockId); //获取锁成功 if (hasLock) { return lockId; } Thread.sleep(DEFAULT_ACQUIRE_RESOLUTION_MILLIS); } } return null; } /** * 释放锁 * * @param redisTemplate redis连接 * @param key redis key * @param lockId 当前锁唯一id */ public static void unlock(RedisTemplate redisTemplate, String key, String lockId) { try { RedisCallback<Boolean> callback = (connection) -> connection.eval(UNLOCK_LUA.getBytes(StandardCharsets.UTF_8),ReturnType.BOOLEAN, 1, (RedisPrefix.LOCK_REDIS_PREFIX + key).getBytes(StandardCharsets.UTF_8), lockId.getBytes(StandardCharsets.UTF_8)); redisTemplate.execute(callback); } catch (Exception e) { log.error("release lock exception", e); } } /** * 获取当前锁的id * * @param key redis key * @return 当前锁唯一id */ public static String get(RedisTemplate redisTemplate, String key) { try { RedisCallback<String> callback = (connection) -> { byte[] bytes = connection.get((RedisPrefix.LOCK_REDIS_PREFIX + key).getBytes(StandardCharsets.UTF_8)); if (bytes != null){ return new String(bytes, StandardCharsets.UTF_8); } return null; }; return (String)redisTemplate.execute(callback); } catch (Exception e) { log.error("get lock id exception", e); } return null; } private static boolean tryLock(RedisTemplate redisTemplate, String key, long expire, String lockId) { RedisCallback<Boolean> callback = (connection) -> connection.set((RedisPrefix.LOCK_REDIS_PREFIX + key).getBytes(StandardCharsets.UTF_8), lockId.getBytes(StandardCharsets.UTF_8), Expiration.seconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT); return (Boolean)redisTemplate.execute(callback); } }