数据一致性高,并发能力弱

代码实践
java /**
* 加redis分布式锁
* @param key
* @return
*/
private boolean tryLock(String key){
Boolean status = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
return BooleanUtil.isTrue(status);
}
/**
* 释放redis分布式锁
* @param key
*/
private void unlock(String key){
stringRedisTemplate.delete(key);
}
java /**
* 互斥锁解决缓存击穿
* @param id
* @return
*/
private Shop queryWithMutex(Long id) {
String lockKey = RedisConstants.LOCK_SHOP_KEY+id;
Shop shop = null;
// 查询redis是否有数据
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 判断是否存在
if (StrUtil.isNotBlank(shopJson)){
// 获取shop对象
// 直接返回商铺信息
return JSONUtil.toBean(shopJson, Shop.class);
}
try {
// 缓存未命中获取互斥锁
// 互斥锁获取成功
boolean isLock = tryLock(lockKey);
if (!isLock){
// 互斥锁获取失败休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
// 不存在查询数据库
shop = getById(id);
if (shop == null){
// 存储空值避免缓存穿透
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 写入redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
// 返回店铺信息
return shop;
}
数据一致性低,并发能力强

java/**
* reids逻辑过期方案
* @param id
* @return
*/
public Shop queryWithLogicalExpire( Long id){
// 查询redis是否有数据
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
// 判断是否存在
if (StrUtil.isBlank(shopJson)){
// 未命中缓存直接返回空
return null;
}
// 命中需要将json反序列化为对象
RedisData bean = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) bean.getData(), Shop.class);
LocalDateTime expireTime = bean.getExpireTime();
// 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
// 未过期
return shop;
}
// 已过期开启缓存重建
// 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock){
// 获取锁成功开启独立线程缓存重建
CACHE_REBUID_EXECUTOR.submit(()->{
try {
// 重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 返回旧数据
return shop;
}
代码实践
java/**
* 优惠券秒杀实现
*
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
// 查询优惠券
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 判断是否开始秒杀
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("优惠券秒杀尚未开始");
}
// 判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("优惠券秒杀已经结束");
}
// 判断库存是否充足
if (seckillVoucher.getStock() < 1){
return Result.fail("优惠券库存不足");
}
Long userId = UserHolder.getUser().getId();
// 悲观锁实现一人一券
synchronized (userId.toString().intern()) {
// 防止事务失效使用代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 一人一单判断
// 查询id
Long id = UserHolder.getUser().getId();
int count = query().eq("user_id", id).eq("voucher_id", voucherId).count();
if (count > 0){
// 说明用户已经购买过
return Result.fail("用户已经购买过一张");
}
// 扣减库存 乐观锁
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock",0) // 如果stock和之前未修改时一样说明不存在数据被修改
.update();
if (!success){
return Result.fail("优惠券库存不足");
}
// 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 用户id
voucherOrder.setUserId(UserHolder.getUser().getId());
// 订单id
voucherOrder.setVoucherId(voucherId);
// 保存订单
save(voucherOrder);
return Result.ok(orderId);
}
使用乐观锁实现扣减库存 使用悲观锁实现一人一单的判断

redis实现分布式锁

初级版本
javapackage com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
/**
* 尝试上锁
*
* @param timeout
* @return
*/
@Override
public boolean tryLock(long timeout) {
// 获取线程显示
long id = Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name,id+"",timeout, TimeUnit.SECONDS); // 如果不存在才执行
return Boolean.TRUE.equals(success);
}
/**
* 释放锁
*/
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
redis锁误删问题

当线程获取锁后业务阻塞执行超时 锁触发超时释放 其他线程获取到锁 但阻塞线程执行完毕执行解锁 将后来线程的锁解除 导致多个线程同时获取到锁
在释放锁时进行锁标识符(UUID 线程id)判断
使用lua脚本实现redis命令的原子性
lua--比较线程标示与锁中的标示是否一致
if (redis.call('get',KEYS[1] == ARGV[1])) then
--释放锁
return redis.call('del',KEYS[1])
end
return 0
java private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
// 初始化文件中的lua脚本
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
}
java /**
* 解锁
*/
@Override
public void unlock() {
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX+name),
ID_PREFIX+Thread.currentThread().getId()
);
}
setnx实现的分布式锁的问题


导入redisson
配置redisson
javapackage com.hmdp.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.42.20:6379").setPassword("123456");
return Redisson.create(config);
}
}
修改业务
javaRLock lock = redissonClient.getLock("order" + userId);
// SimpleRedisLock simpleRedisLock = new SimpleRedisLock(stringRedisTemplate, "order" + userId);
// 获取锁
boolean isLock = lock.tryLock();
// 判断获取锁是否成功
if (!isLock){
// 获取锁失败,返回错误或重试
return Result.fail("不允许重复下单");
}
try {
// 防止事务失效使用代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}finally {
lock.unlock();
}
可重入锁原理

lua脚本实践


配置3个redis
添加联锁
javaredissonClient.getMultiLock(redissonClient,redissonClient2,redissonClient3);

编写lua脚本实现购票判断
lua--1.参数列表
--1.1优惠券id
local voucherId = ARGV[1]
--用户id
local userId = ARGV[2]
local stockKey = 'seckill:stock:'..voucherId
local orderKey = 'seckill:order:'..voucherId
if (tonumber(redis.call('get',stockKey))<=0) then
--库存不足
return 1;
end
--判断用户是否下单
if (redis.call('sismember',orderKey,userId) == 1) then
return 2
end
--扣减库存
redis.call('incrby',stockKey,-1)
--下单
redis.call('sadd',orderKey,userId)
return 0
使用阻塞队列实现异步添加订单
java /**
* 优惠券秒杀实现
*
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 判断是否开始秒杀
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("优惠券秒杀尚未开始");
}
// 判断秒杀是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("优惠券秒杀已经结束");
}
// 使用redis优化秒杀逻辑
// 执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT
, Collections.emptyList()
, voucherId.toString()
, UserHolder.getUser().getId().toString()
);
// 判断返回值
int r = result.intValue();
if (r != 0){
// 不为0代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
// 为0有购买资格
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
// 保存到阻塞队列
// 用户id
voucherOrder.setId(orderId);
// 代金券id
voucherOrder.setVoucherId(voucherId);
// 放入到阻塞队列中
orderTasks.add(voucherOrder);
// 获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy();
// 返回订单id
return Result.ok(orderId);
}
使用redis中的brpop和lpush实现


本文作者:钱小杰
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!