在日常开发中,多多少少都在使用redis中间件,比如用作缓存,分布式锁,唯一id、消息通知等,现将用到的场景记录下来,后续会更新哦。
string类型
```
set key value //存入键值对
get key value //根据键获取值
del key //删除
expire key timeout // 为key设置一个超时时间,超过时间会自动释放
setnx key value //当且仅当key不存在时,set一个key为value的字符串,返回1;若key存在,则什么都不做,返回0
incr key //将key中存储的值加一
decr key //将key中存储的值减一
```
分布式锁
```
setnx order_lock true //返回1代表获取锁成功
setnx order_lock true //返回0代表获取锁失败
del order_lock //执行完业务释放锁
好比:张三去上厕所,看厕所门锁着,他就不进去了,厕所门开着他才进入;
问题1,若redis因为宕机或出现异常未释放锁,就造成了死锁;可以通过设置过期时间解决未释放锁的情况,但需要组合命令set key value ex seconds nx;问题2,一个业务执行时间很长,锁已经自动过期了,别人获取到了锁,但是当业务执行完之后直接释放了锁,这时就可能删除了别人的锁,可以通过在加锁的时候设置一个随机值,在删除锁的时候进行对比,若是自己的锁才删除;
redis实现分布式锁的方式为去插入一条占位数据;遇到宕机情况,redis需要等到设置的过期时间到了后自动释放锁;redis在没抢占到锁的情况时一般会去自旋获取锁;
```
实现思想
```
获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断
获取锁的时间设置一个获取的超时时间,若超过这个时间则放弃获取锁
释放锁的时候,通过UUID判断是不是该锁,若是该锁则执行delete进行锁释放
```
示例,在指定时间acquireTimeout内进行秒杀活动,每个抢到订单的需要在指定时间expire内完成支付,如下:
public class RedisLock {
private final JedisPool jedisPool;
public RedisLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加锁
*
* @param keyName 锁的key
* @param acquireTimeout 获取超时时间
* @param expire 锁的过期时间
* @return 锁标识
*/
public String lockWithTimeout(String keyName, long acquireTimeout, long expire) {
// 随机生成一个value
String value = UUID.randomUUID().toString();
// 锁名,即key值
String lockKey = "lock:" + keyName;
// 锁的超时时间,上锁后超过此时间则自动释放锁
int lockExpire = (int) (expire / 1000);
// 获取锁的超时时间,超过这个时间则放弃获取锁
long end = System.currentTimeMillis() + acquireTimeout;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
while (System.currentTimeMillis() < end) {
Long setnx = jedis.setnx(lockKey, value);
if (setnx == 1) {
jedis.expire(lockKey,lockExpire);
return value;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
return null;
}
/**
* 释放锁
*
* @param keyName 锁的key
* @param identifier 释放锁的标识
* @return
*/
public boolean releaseLock(String keyName, String identifier) {
String lockKey = "lock:" + keyName;
boolean retFlag = false;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
while (true) {
// 监视lock,准备开始事务
jedis.watch(lockKey);
// 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
if (identifier.equals(jedis.get(lockKey))) {
Transaction transaction = jedis.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) {
continue;
}
retFlag = true;
}
jedis.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
return retFlag;
}
}
public class DemoService {
private static JedisPool jedisPool = null;
static {
JedisPoolConfig config = new JedisPoolConfig();
// 设置最大连接数
config.setMaxTotal(200);
// 设置最大空闲数
config.setMaxIdle(8);
// 设置最大等待时间
config.setMaxWaitMillis(1000 * 100);
jedisPool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}
RedisLock redisLock = new RedisLock(jedisPool);
int n = 500;
public void seckill() {
// 返回锁的value值,供释放锁时候进行判断
String indentifier = null;
indentifier = redisLock.lockWithTimeout("resource", 5000, 1000);
System.out.println(Thread.currentThread().getName() + "获得了锁");
if (indentifier != null) {
// 获取到锁后才可以进行操作(只有占用厕所,才可以如厕),具体能够有几个操作需要看acquireTimeout、expire
System.out.println(--n);
redisLock.releaseLock("resource", indentifier);
}
}
}
public class DemoThread extends Thread {
private DemoService demoService;
public DemoThread(DemoService demoService) {
this.demoService = demoService;
}
@Override
public void run() {
demoService.seckill();
}
public static void main(String[] args) {
DemoService demoService = new DemoService();
for (int i = 0; i < 50; i++) {
DemoThread demoThread = new DemoThread(demoService);
demoThread.start();
}
}
}
结果如下:
Thread-7获得了锁
499
Thread-49获得了锁
498
Thread-50获得了锁
497
...
springboot继承redis中实现代码如下:
@Component
public class RedisLockService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 加锁
*
* @param keyName 锁的key
* @param acquireTimeout 获取超时时间
* @param expire 锁的过期时间
* @return 锁标识
*/
public String lockWithTimeout(String keyName, long acquireTimeout, long expire) {
// 随机生成一个value
String value = UUID.randomUUID().toString();
// 锁名,即key值
String lockKey = "lock:" + keyName;
// 锁的超时时间,上锁后超过此时间则自动释放锁
int lockExpire = (int) (expire / 1000);
// 获取锁的超时时间,超过这个时间则放弃获取锁
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, value, lockExpire, TimeUnit.SECONDS);
if (ifAbsent) {
return value;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return null;
}
/**
* 释放锁
*
* @param keyName 锁的key
* @param identifier 释放锁的标识
* @return
*/
public boolean releaseLock(String keyName, String identifier) {
String lockKey = "lock:" + keyName;
boolean retFlag = false;
Object value = redisTemplate.opsForValue().get(lockKey);
if (identifier.equals(value)) {
retFlag = redisTemplate.delete(keyName);
}
return retFlag;
}
}
@Component
public class DemoSkillService {
int n = 500;
@Autowired
private RedisLockService redisLockService;
public void seckill() {
// 返回锁的value值,供释放锁时候进行判断
String indentifier = null;
indentifier = redisLockService.lockWithTimeout("resource", 5000, 1000);
if (indentifier != null) {
// 获取到锁后才可以进行操作(只有占用厕所,才可以如厕),具体能够有几个操作需要看acquireTimeout、expire
System.out.println(Thread.currentThread().getName() + "获得了锁");
System.out.println(--n);
redisLockService.releaseLock("resource", indentifier);
}
}
}
结果如下;
Thread-154获得了锁
484
Thread-153获得了锁
483
Thread-185获得了锁
482
Thread-191获得了锁
481
Thread-161获得了锁
480
Thread-196获得了锁
479
Thread-194获得了锁
478
redis唯一编号使用场景,在分布式开发中可以通过redis的自增自减操作生成唯一id作为主键或编码来使用,由于是顺序数组组成,后续使用方便快捷。平常开发中也会使用雪花算法生产唯一id作为主键使用。
代码如下:
public interface OrderService {
public String orderId();
}
@Service
public class OderServiceImpl implements OrderService {
@Autowired
private RedisTemplate redisTemplate;
@Override
public String orderId() {
String key = "order:id";
String prefix = getPrefix();
Long id = redisTemplate.opsForValue().increment(key);
System.out.println("prefix:" + prefix);
System.out.println("id:" + prefix + id);
return prefix + id;
}
private String getPrefix() {
LocalDateTime now = LocalDateTime.now();
int year = now.getYear();
int month = now.getMonthValue();
int day = now.getDayOfYear();
// 处理
return String.valueOf(year) + String.valueOf(month) + String.valueOf(day);
}
}
单位测试代码部分如下:
private static final int num = 100;
private CountDownLatch countDownLatch = new CountDownLatch(num);
@Autowired
private OrderService orderService;
@Test
void contextLoads() {
for (int i = 0; i <num; i++) {
Thread thread = new Thread(()-> {
try {
countDownLatch.await();
orderService.orderId();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
countDownLatch.countDown();
}
}
结果如下:
prefix:202119
id:2021191081
id:2021191079
prefix:202119
id:2021191077
prefix:202119
id:2021191076
prefix:202119
id:2021191075
id:2021191073
redis消息通知场景,部分代码如下:
@Configuration
public class RedisListenerConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
return redisMessageListenerContainer;
}
}
@Component
public class RedisTask extends KeyExpirationEventMessageListener {
public RedisTask(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
// 接受事件后回调
String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
String key = new String(message.getBody(), StandardCharsets.UTF_8);
System.out.println("key:" + key + ",chanel:" + channel);
// 根据key进行处理
}
}
分布式锁应用案例:
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
String key = "key:" + request.getSeckillId;
try {
Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, "val", 10, TimeUnit.SECONDS);
if (lockFlag) {
// HTTP请求用户服务进行用户相关的校验
// 用户活动校验
// 库存校验
Object stock = redisTemplate.opsForHash().get(key+":info", "stock");
assert stock != null;
if (Integer.parseInt(stock.toString()) <= 0) {
// 业务异常
} else {
redisTemplate.opsForHash().increment(key+":info", "stock", -1);
// 生成订单
// 发布订单创建成功事件
// 构建响应VO
}
}
} finally {
// 释放锁
stringRedisTemplate.delete("key");
// 构建响应VO
}
return response;
}
//改进
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
String key = "key:" + request.getSeckillId();
String val = UUID.randomUUID().toString();
try {
Boolean lockFlag = distributedLocker.lock(key, val, 10, TimeUnit.SECONDS);
if (!lockFlag) {
// 业务异常
}
// 用户活动校验
// 库存校验,基于redis本身的原子性来保证
Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);
if (currStock < 0) { // 说明库存已经扣减完了。
// 业务异常。
log.error("[抢购下单] 无库存");
} else {
// 生成订单
// 发布订单创建成功事件
// 构建响应
}
} finally {
distributedLocker.safedUnLock(key, val);
// 构建响应
}
return response;
}
//改进
// 通过消息提前初始化好,借助ConcurrentHashMap实现高效线程安全
private static ConcurrentHashMap<Long, Boolean> SECKILL_FLAG_MAP = new ConcurrentHashMap<>();
// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMap
private static Map<Long, AtomicInteger> SECKILL_STOCK_MAP = new HashMap<>();
...
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
Long seckillId = request.getSeckillId();
if(!SECKILL_FLAG_MAP.get(requestseckillId)) {
// 业务异常
}
// 用户活动校验
// 库存校验
if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet() < 0) {
SECKILL_FLAG_MAP.put(seckillId, false);
// 业务异常
}
// 生成订单
// 发布订单创建成功事件
// 构建响应
return response;
}
超卖原因:虽然采用了setnx key value [EX seconds] [PX milliseconds] [NX|XX]的方式,但是如果线程A执行的时间较长没有来得及释放,锁就过期了,此时线程B是可以获取到锁的。当线程A执行完成之后,释放锁,实际上就把线程B的锁释放掉了。这个时候,线程C又是可以获取到锁的,而此时如果线程B执行完释放锁实际上就是释放的线程C设置的锁。这是超卖的直接原因。